<?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>[AWS] Should you return results or answers? The difference between Web Search and Web Answer [AgentCore]</title>
      <dc:creator>Nao San</dc:creator>
      <pubDate>Sun, 08 Nov 2026 06:49:55 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-should-you-return-results-or-answers-the-difference-between-web-search-and-web-answer-3425</link>
      <guid>https://dev.to/aws-builders/aws-should-you-return-results-or-answers-the-difference-between-web-search-and-web-answer-3425</guid>
      <description>&lt;p&gt;This article is a machine translation of the contents of the following URL, which I wrote in Japanese:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://qiita.com/Nana_777/items/6e466fb974972a79a323" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fqiita-user-contents.imgix.net%2Fhttps%253A%252F%252Fqiita-user-contents.imgix.net%252Fhttps%25253A%25252F%25252Fcdn.qiita.com%25252Fassets%25252Fpublic%25252Farticle-ogp-background-afbab5eb44e0b055cce1258705637a91.png%253Fixlib%253Drb-4.1.1%2526w%253D1200%2526blend64%253DaHR0cHM6Ly9xaWl0YS11c2VyLXByb2ZpbGUtaW1hZ2VzLmltZ2l4Lm5ldC9odHRwcyUzQSUyRiUyRnMzLWFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20lMkZxaWl0YS1pbWFnZS1zdG9yZSUyRjAlMkYxOTI5NDklMkYxYmVjZGE4YTRmMWRiOWU5OTQyZDljOGMwYjZkZDA3NjkwMTM2NjgwJTJGbGFyZ2UucG5nJTNGMTc0MTQ4MzQxMD9peGxpYj1yYi00LjEuMSZhcj0xJTNBMSZmaXQ9Y3JvcCZtYXNrPWVsbGlwc2UmYmc9RkZGRkZGJmZtPXBuZzMyJnM9MDZlM2QxY2EzNTVhN2Y4N2M2ZTQxY2RlYzdmN2M0ODI%2526blend-x%253D120%2526blend-y%253D462%2526blend-w%253D90%2526blend-h%253D90%2526blend-mode%253Dnormal%2526mark64%253DaHR0cHM6Ly9xaWl0YS1vcmdhbml6YXRpb24taW1hZ2VzLmltZ2l4Lm5ldC9odHRwcyUzQSUyRiUyRnMzLWFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20lMkZxaWl0YS1vcmdhbml6YXRpb24taW1hZ2UlMkYxZTIwZGI1ZTdlNDA3ZWFlN2I5NzBlYzk5OTg4MGRjOGMzY2MwZjY0JTJGb3JpZ2luYWwuanBnJTNGMTY2NzI3NDk2Mz9peGxpYj1yYi00LjEuMSZ3PTQ0Jmg9NDQmZml0PWNyb3AmbWFzaz1jb3JuZXJzJmNvcm5lci1yYWRpdXM9OCZiZz1GRkZGRkYmYm9yZGVyPTIlMkNGRkZGRkYmZm09cG5nMzImcz04OTI0NDcwMDk1MDMyZjUxYmZlNzE3ZjVkOGUzYmE3ZA%2526mark-x%253D186%2526mark-y%253D515%2526mark-w%253D40%2526mark-h%253D40%2526s%253D0db13b4aef231a19bc684897cf15021a%3Fixlib%3Drb-4.1.1%26w%3D1200%26fm%3Djpg%26mark64%3DaHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjEuMSZ3PTk2MCZoPTMyNCZ0eHQ9JUUzJTgwJTkwQVdTJUUzJTgwJTkxJUU3JUI1JTkwJUU2JTlFJTlDJUUzJTgyJTkyJUU4JUJGJTk0JUUzJTgxJTk5JUUzJTgxJThCJUUzJTgwJTgxJUU3JUFEJTk0JUUzJTgxJTg4JUUzJTgyJTkyJUU4JUJGJTk0JUUzJTgxJTk5JUUzJTgxJThCJUUzJTgwJTgyV2ViJTIwU2VhcmNoJUUzJTgxJUE4V2ViJTIwQW5zd2VyJUUzJTgxJUFFJUU0JUJEJUJGJUUzJTgxJTg0JUU1JTg4JTg2JUUzJTgxJTkxJUUzJTgwJTkwQWdlbnRDb3JlJUUzJTgwJTkxJnR4dC1hbGlnbj1sZWZ0JTJDdG9wJnR4dC1jb2xvcj0lMjMxRTIxMjEmdHh0LWZvbnQ9SGlyYWdpbm8lMjBTYW5zJTIwVzYmdHh0LXNpemU9NTYmdHh0LXBhZD0wJnM9NzdhZjQxZGMxZDAwZGJmYTJiYjE4NTI2YzVkOTk3ODQ%26mark-x%3D120%26mark-y%3D112%26blend64%3DaHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjEuMSZ3PTgzOCZoPTU4JnR4dD0lNDBOYW5hXzc3NyZ0eHQtY29sb3I9JTIzMUUyMTIxJnR4dC1mb250PUhpcmFnaW5vJTIwU2FucyUyMFc2JnR4dC1zaXplPTM2JnR4dC1wYWQ9MCZzPTg1YzQ4MzUyMmVkMGVhNmYxYjhkZjU5MjVjYzhiNjQz%26blend-x%3D242%26blend-y%3D454%26blend-w%3D838%26blend-h%3D46%26blend-fit%3Dcrop%26blend-crop%3Dleft%252Cbottom%26blend-mode%3Dnormal%26txt64%3DS0RESeOCouOCuOODo-OCpOODq-mWi-eZuuOCu-ODs-OCv-ODvOagquW8j-S8muekvg%26txt-x%3D242%26txt-y%3D539%26txt-width%3D838%26txt-clip%3Dend%252Cellipsis%26txt-color%3D%25231E2121%26txt-font%3DHiragino%2520Sans%2520W6%26txt-size%3D28%26s%3D25cce219e7e398d8b8c1cf4fc17bd201" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://qiita.com/Nana_777/items/6e466fb974972a79a323" rel="noopener noreferrer" class="c-link"&gt;
            【AWS】結果を返すか、答えを返すか。Web SearchとWeb Answerの使い分け【AgentCore】 #AIエージェント - Qiita
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            はじめに AIエージェントに最新のウェブ情報を渡したい場面は多いです。これまでは、検索APIを呼び、ページを取得し、要約して、出典を付ける、というパイプラインを自分で組む必要がありました。 Amazon Bedrock AgentCoreには、この部分をマネージドで提供す...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.qiita.com%2Fassets%2Ffavicons%2Fpublic%2Fproduction-c620d3e403342b1022967ba5e3db1aaa.ico" width="120" height="120"&gt;
          qiita.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;There are many situations where you want to give an AI agent up-to-date web information. Until now, you had to build the pipeline yourself: call a search API, fetch pages, summarize them, and attach sources.&lt;/p&gt;

&lt;p&gt;Amazon Bedrock AgentCore offers two managed features for this part.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web Search is a tool that returns ranked search results. It was introduced in June 2026&lt;/li&gt;
&lt;li&gt;Web Answer is a tool that returns a finished answer with citations. It became generally available (GA) on November 5, 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are added as connectors on AgentCore Gateway and called via MCP. AWS describes them as "complementary", but when you actually have to choose, questions like these come up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the only difference is what comes back, can't I do the same things with either one?&lt;/li&gt;
&lt;li&gt;If I want to control which domains are referenced, how far can each tool go?&lt;/li&gt;
&lt;li&gt;Which one can I use in the Tokyo Region? (As of 2026-11-08. Supported Regions may increase.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article organizes the specifications from the official documentation in tables, adds the results I confirmed on a real environment, and summarizes criteria for choosing between them.&lt;/p&gt;

&lt;p&gt;So that readers who are new to Web Answer can follow along, the first half compares the two in tables, and the second half shows the verification results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key differences between Web Search and Web Answer
&lt;/h2&gt;

&lt;p&gt;Here are the key points as a table first. The "Confirmed on a real environment" column shows what I confirmed in us-east-1 on 2026-11-08.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;th&gt;Confirmed on a real environment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it returns&lt;/td&gt;
&lt;td&gt;Ranked results (text, url, title, publishedDate)&lt;/td&gt;
&lt;td&gt;A synthesized answer with citations&lt;/td&gt;
&lt;td&gt;Confirmed (Verification 1). Search has no &lt;code&gt;structuredContent&lt;/code&gt;, and &lt;code&gt;publishedDate&lt;/code&gt; is in a custom format or &lt;code&gt;unknown&lt;/code&gt;. Answer's citation markers map to the order of first appearance after removing duplicates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who composes the answer&lt;/td&gt;
&lt;td&gt;The model of your own agent&lt;/td&gt;
&lt;td&gt;The Web Answer side&lt;/td&gt;
&lt;td&gt;Confirmed (Verification 1). Web Search returned only results (snippets), and Web Answer returned an answer with citations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price (tool side)&lt;/td&gt;
&lt;td&gt;$7 / 1,000 queries&lt;/td&gt;
&lt;td&gt;$10 / 1,000 invocations&lt;/td&gt;
&lt;td&gt;Official documentation (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regions (as of 2026-11-08)&lt;/td&gt;
&lt;td&gt;7 Regions including Tokyo&lt;/td&gt;
&lt;td&gt;us-east-1 only (the blog says "more AWS Regions coming soon")&lt;/td&gt;
&lt;td&gt;Official documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain control&lt;/td&gt;
&lt;td&gt;Two layers: administrator and request (100 entries each)&lt;/td&gt;
&lt;td&gt;Two layers: administrator and request (50 entries each)&lt;/td&gt;
&lt;td&gt;Confirmed (Verification 3). Both kept to the administrator's boundary, but violations look different (Answer returns an error; Search narrows the results or returns an empty result)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The choice is almost entirely decided by two questions: "Do you want to handle the results with your own model, or do you want a finished answer?" and "Do you need to keep everything in Tokyo?" For domain control, both could be fixed on the administrator side, but they behave differently when a request goes outside the boundary. Note that Web Search returns an empty result without an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;First, here is the architecture as a diagram. The calling agent goes through the Gateway and calls two targets (connectors). The web index is operated by Amazon and sits outside your own account. The answer from Web Answer is written by a model on the Amazon side, and you cannot choose that model. The results from Web Search are turned into an answer by your own model, which you choose on the calling side.&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%2F04jz100s03wrj4ezu692.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%2F04jz100s03wrj4ezu692.png" alt="Overview diagram (Japanese labels)" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Specification comparison by aspect
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;No.&lt;/th&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&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;What it returns&lt;/td&gt;
&lt;td&gt;Ranked results&lt;/td&gt;
&lt;td&gt;A synthesized answer with citations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;connectorId&lt;/td&gt;
&lt;td&gt;&lt;code&gt;web-search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;web-answer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Query length&lt;/td&gt;
&lt;td&gt;Up to 200 characters&lt;/td&gt;
&lt;td&gt;1 to 30,000 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Result count control&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;maxResults&lt;/code&gt; 1 to 25 (default 10)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Domain lists&lt;/td&gt;
&lt;td&gt;100 each (request side)&lt;/td&gt;
&lt;td&gt;50 each (100 for exclude after merging)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Date filter&lt;/td&gt;
&lt;td&gt;Yes (1.2.0 or later)&lt;/td&gt;
&lt;td&gt;Yes (1.1.0 or later)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Location&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;&lt;code&gt;userLocation.country&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Regions (as of 2026-11-08)&lt;/td&gt;
&lt;td&gt;7 Regions (including Tokyo)&lt;/td&gt;
&lt;td&gt;us-east-1 only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$7 / 1,000 queries&lt;/td&gt;
&lt;td&gt;$10 / 1,000 invocations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;IAM permission on the service role (independent per tool)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InvokeWebSearch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InvokeWebAnswer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Detailed explanation of each aspect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Aspect 1: What each tool returns
&lt;/h3&gt;

&lt;p&gt;AWS positions Web Search as follows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Web Search Tool on Amazon Bedrock AgentCore is different — it is backed by a web index that Amazon builds and operates directly."&lt;br&gt;
—— &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;&lt;code&gt;Web Search Tool&lt;/code&gt; (AgentCore Developer Guide)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Web Answer is described as searching the web, reading pages, and answering with citations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"searches the web, reads relevant pages to ground its answer, and includes citations."&lt;br&gt;
—— &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-answer-tool.html" rel="noopener noreferrer"&gt;&lt;code&gt;Web Answer&lt;/code&gt; (AgentCore Developer Guide)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the difference in response format, field by field.&lt;/p&gt;

&lt;p&gt;Web Search (&lt;code&gt;content[0].text&lt;/code&gt; is a JSON string)&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;"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;"824f89d0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-10-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;Web Answer (&lt;code&gt;content[0].text&lt;/code&gt; is Markdown, and &lt;code&gt;structuredContent&lt;/code&gt; holds the list of citations)&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;"isError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"... [[1](https://example.com/basel-iii)]..."&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"structuredContent"&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;"answer"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"citations"&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;"domain"&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.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&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://example.com/basel-iii"&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="nl"&gt;"_meta"&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;"latencyMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5210&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;Both of the above are examples from the official documentation. When I checked on a real environment (us-east-1, 2026-11-08, connectors Search 1.3.0 / Answer 1.1.0), I found the following differences (details in Verification 1).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web Search results have no &lt;code&gt;structuredContent&lt;/code&gt;; only the JSON string in &lt;code&gt;content[0].text&lt;/code&gt; is returned&lt;/li&gt;
&lt;li&gt;Web Search's &lt;code&gt;publishedDate&lt;/code&gt; is not ISO-8601. It is a string such as &lt;code&gt;05:00PM, Wednesday, September 09 2026, PDT&lt;/code&gt;, or &lt;code&gt;unknown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Web Answer's citation marker &lt;code&gt;[[N]]&lt;/code&gt; did not correspond to &lt;code&gt;citations[N-1]&lt;/code&gt;. It corresponded to the Nth URL in order of first appearance after removing duplicates (looking up by index mismatched in 7 of 9 runs, while the deduplicated order matched in all 9)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Aspects 2 to 4: Differences from the caller's point of view
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Web Search is meant to be given short search keywords (up to 200 characters), and &lt;code&gt;maxResults&lt;/code&gt; lets you limit the number of results&lt;/li&gt;
&lt;li&gt;Web Answer accepts a longer natural-language question (up to 30,000 characters). There is no way to specify the number of results&lt;/li&gt;
&lt;li&gt;Tool names have the form &lt;code&gt;&amp;lt;target name&amp;gt;___&amp;lt;tool name&amp;gt;&lt;/code&gt; (three underscores). On the real environment they were &lt;code&gt;web-answer-tool___WebAnswer&lt;/code&gt; and &lt;code&gt;web-search-tool___WebSearch&lt;/code&gt;. On a Gateway with semantic search enabled, &lt;code&gt;tools/list&lt;/code&gt; can also include another tool (&lt;code&gt;x_amz_bedrock_agentcore_search&lt;/code&gt;), so you need to pick by the end of the name (the Gateway I used for verification had semantic search disabled, so it was not included)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;tools/call&lt;/code&gt; succeeded when I sent SigV4-signed HTTP requests without &lt;code&gt;initialize&lt;/code&gt;. Web Search's &lt;code&gt;tools/list&lt;/code&gt; includes &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;annotations&lt;/code&gt; (&lt;code&gt;readOnlyHint: true&lt;/code&gt;, &lt;code&gt;openWorldHint: true&lt;/code&gt;) (connector 1.3.0)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Aspects 5 and 6: Domain and date filters
&lt;/h3&gt;

&lt;p&gt;Both have two layers: a boundary that the administrator sets on the target, and conditions that the agent specifies on each call.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Request-level filters cannot override the target-level exclude list or expand results beyond the target-level include list."&lt;br&gt;
—— &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;&lt;code&gt;Web Search Tool&lt;/code&gt; (AgentCore Developer Guide)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request-side list limit&lt;/td&gt;
&lt;td&gt;100 each&lt;/td&gt;
&lt;td&gt;50 each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handling of exclude&lt;/td&gt;
&lt;td&gt;The administrator's exclude and the request's exclude are merged, and domains in the merged list are excluded&lt;/td&gt;
&lt;td&gt;Same as left. Up to 100 entries after merging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When both sides have include&lt;/td&gt;
&lt;td&gt;Only the intersection is used. An empty intersection is an error&lt;/td&gt;
&lt;td&gt;Officially "only the intersection is used; an empty intersection is an error". In my tests, even with a non-empty intersection, it returned an error if even one non-permitted domain was included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date specification&lt;/td&gt;
&lt;td&gt;ISO-8601, UTC, both ends inclusive&lt;/td&gt;
&lt;td&gt;Date only, or date-time (&lt;code&gt;Z&lt;/code&gt; or &lt;code&gt;+HH:mm&lt;/code&gt; allowed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required connector version&lt;/td&gt;
&lt;td&gt;Filters from 1.2.0&lt;/td&gt;
&lt;td&gt;Filters from 1.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, if a regulated-industry customer wants the agent to reference only trusted domains, you can pin that with the administrator-side include. In Verification 3, I confirmed that domains outside the boundary specified from the request side, and domains that hit the administrator's exclude when included on the request side, did not appear in the results (one observation per pattern).&lt;/p&gt;

&lt;h3&gt;
  
  
  Aspect 8: Regions and data handling
&lt;/h3&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;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supported Regions (as of 2026-11-08)&lt;/td&gt;
&lt;td&gt;us-east-1, us-east-2, us-west-2, eu-west-1, ap-southeast-2, ap-northeast-1 (Tokyo), us-gov-west-1&lt;/td&gt;
&lt;td&gt;us-east-1 only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope of processing&lt;/td&gt;
&lt;td&gt;Amazon-operated web index&lt;/td&gt;
&lt;td&gt;Amazon-operated web index and knowledge graph. Answers are generated by a model on Bedrock using geographic cross-Region inference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The supported Regions above are as of 2026-11-08. "us-east-1 only" for Web Answer describes the current situation, and the blog says "more AWS Regions coming soon". Because the supported Regions may increase, check the latest &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html" rel="noopener noreferrer"&gt;Region table&lt;/a&gt; before use. The verification in this article was done in us-east-1, which Web Answer supports.&lt;/p&gt;

&lt;p&gt;If you want to keep everything within the Tokyo Region, Web Search is the only choice at present (2026-11-08). If you use Web Answer in us-east-1, you need to check the customer's requirements, because data processing involves a US Region.&lt;/p&gt;

&lt;p&gt;"Zero data egress" means that queries and results do not leave the AWS environment. The wording differs between the blog (queries and results stay within the AWS environment) and the product page (they can also leave via a VPC or PrivateLink). I did not verify which wording matches the actual behavior. If the data exit point matters for a customer's requirements, check the official documentation or ask AWS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aspect 9: Price
&lt;/h3&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;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool unit price&lt;/td&gt;
&lt;td&gt;$7.00 / 1,000 queries ($0.007 per call)&lt;/td&gt;
&lt;td&gt;$10 / 1,000 invocations ($0.010 per call)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway API&lt;/td&gt;
&lt;td&gt;$0.005 / 1,000 calls&lt;/td&gt;
&lt;td&gt;Same as left (because calls go through the Gateway)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gateway API charges depend on the number of &lt;code&gt;ListTools&lt;/code&gt;, &lt;code&gt;InvokeTool&lt;/code&gt;, and &lt;code&gt;Ping&lt;/code&gt; calls. The unit prices are listed on the &lt;a href="https://aws.amazon.com/bedrock/agentcore/pricing/" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aspect 10: Separation of IAM responsibilities
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gateway service role (Web Search)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bedrock-agentcore:InvokeWebSearch&lt;/code&gt; (resource: &lt;code&gt;arn:aws:bedrock-agentcore:us-east-1:aws:tool/web-search.v1&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway service role (Web Answer)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bedrock-agentcore:InvokeWebAnswer&lt;/code&gt; (resource: &lt;code&gt;arn:aws:bedrock-agentcore:us-east-1:aws:tool/web-answer.v1&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caller (execution role of the agent or harness)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bedrock-agentcore:InvokeGateway&lt;/code&gt; (Gateway ARN)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Verification 2 showed that &lt;code&gt;InvokeGateway&lt;/code&gt; is required on the caller and not on the service role. &lt;code&gt;InvokeWebAnswer&lt;/code&gt; and &lt;code&gt;InvokeWebSearch&lt;/code&gt; on the service role are each required independently per tool (details in Verification 2).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I confirmed on a real environment
&lt;/h2&gt;

&lt;p&gt;The following is what I actually confirmed in us-east-1 on 2026-11-08. Each verification is written in this order: what I checked, the measurements, and what can be concluded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification 1: Sending the same question to both and comparing the response formats
&lt;/h3&gt;

&lt;p&gt;I checked the difference in response format and how citations are attached, using actual responses. I sent the same three questions to both Web Search and Web Answer. I fixed the questions to English because Web Answer supported English at launch.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Question text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Fact check&lt;/td&gt;
&lt;td&gt;What is the maximum execution timeout of an AWS Lambda function?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;Regulation / policy&lt;/td&gt;
&lt;td&gt;What is the minimum Common Equity Tier 1 capital ratio required under Basel III?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;Latest information&lt;/td&gt;
&lt;td&gt;What are the recent updates to Amazon Bedrock AgentCore?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I sent each question to each tool three times, for 18 calls in total. All returned HTTP 200 with &lt;code&gt;isError&lt;/code&gt; false. Web Search used &lt;code&gt;maxResults: 5&lt;/code&gt;. I recorded the response format, whether and where citations appear, whether &lt;code&gt;publishedDate&lt;/code&gt; is present, and latency. For latency, I recorded both &lt;code&gt;_meta.latencyMs&lt;/code&gt; and client-side timing for Web Answer, and only client-side timing for Web Search because it returns no &lt;code&gt;_meta&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Latency (milliseconds; average of 3 runs; client side is the time of the whole HTTP request)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Search, client side&lt;/th&gt;
&lt;th&gt;Answer, client side&lt;/th&gt;
&lt;th&gt;Answer, server side (&lt;code&gt;_meta.latencyMs&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 (fact check)&lt;/td&gt;
&lt;td&gt;1,101&lt;/td&gt;
&lt;td&gt;3,122&lt;/td&gt;
&lt;td&gt;2,292&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 (regulation)&lt;/td&gt;
&lt;td&gt;1,136&lt;/td&gt;
&lt;td&gt;3,011&lt;/td&gt;
&lt;td&gt;2,215&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 (latest information)&lt;/td&gt;
&lt;td&gt;1,259&lt;/td&gt;
&lt;td&gt;6,555&lt;/td&gt;
&lt;td&gt;5,744&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Differences in the response&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;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Top-level keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isError&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isError&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;structuredContent&lt;/code&gt;, &lt;code&gt;_meta&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;content[0].text&lt;/code&gt; is a JSON string of the form &lt;code&gt;{"id": ..., "results": [...]}&lt;/code&gt;, and &lt;code&gt;results&lt;/code&gt; has &lt;code&gt;maxResults&lt;/code&gt; entries (5)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;content[0].text&lt;/code&gt; is Markdown. Citations are &lt;code&gt;[[N](url)]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contents of one item&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;text&lt;/code&gt; (snippet, about 700 to 5,000 characters), &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;publishedDate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The answer body and a list of citations (&lt;code&gt;structuredContent.answer&lt;/code&gt; is one block)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dates&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;publishedDate&lt;/code&gt; is in the form &lt;code&gt;05:00PM, Wednesday, September 09 2026, PDT&lt;/code&gt;, or &lt;code&gt;unknown&lt;/code&gt; (0 to 2 of 5 items were &lt;code&gt;unknown&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No date is attached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When the same question is sent 3 times&lt;/td&gt;
&lt;td&gt;The set of result URLs was the same all 3 times. The whole response was not fully identical, though (the length was the same in some runs)&lt;/td&gt;
&lt;td&gt;The body length changes between runs (whole-response character counts for Q1: 1,947 / 1,485 / 2,313)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Characters in the whole response&lt;/td&gt;
&lt;td&gt;About 9,600 to 14,100&lt;/td&gt;
&lt;td&gt;About 1,400 to 2,500 for Q1 and Q2, about 10,000 to 12,700 for Q3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Actual content returned (Q1 "What is the maximum execution timeout of an AWS Lambda function?", first run)&lt;/p&gt;

&lt;p&gt;Web Answer's answer (full &lt;code&gt;content[0].text&lt;/code&gt;, 627 characters).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The standard maximum timeout for an AWS Lambda function is 900 seconds (15 minutes). [[1](https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html)]

There is one exception: for functions on Lambda Managed Instances, asynchronous and event source mapping invocations can be set to up to 5,400 seconds (90 minutes). [[2](https://aws.amazon.com/blogs/compute/announcing-90-minute-function-timeout-on-aws-lambda-managed-instances/)] Synchronous invocations on those instances still cap at 15 minutes. [[2](https://aws.amazon.com/blogs/compute/announcing-90-minute-function-timeout-on-aws-lambda-managed-instances/)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Web Search results (&lt;code&gt;maxResults: 5&lt;/code&gt;. The 5 snippets total 8,227 characters. The full text is in the collapsible section below).&lt;/p&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;Title&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;publishedDate&lt;/th&gt;
&lt;th&gt;Snippet characters&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;Configure Lambda Event Function timeout&lt;/td&gt;
&lt;td&gt;docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html&lt;/td&gt;
&lt;td&gt;05:00PM, Sunday, October 30 2016, PDT&lt;/td&gt;
&lt;td&gt;1,164&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Troubleshoot configuration issues in Lambda&lt;/td&gt;
&lt;td&gt;docs.aws.amazon.com/lambda/latest/operatorguide/configurations.html&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;2,312&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AWS SDK for Kotlin&lt;/td&gt;
&lt;td&gt;docs.aws.amazon.com/sdk-for-kotlin/api/latest/lambda/.../timeout.html&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;1,365&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Announcing 90-minute function timeout on AWS Lambda Managed Instances&lt;/td&gt;
&lt;td&gt;aws.amazon.com/blogs/compute/announcing-90-minute-function-timeout-on-aws-lambda-managed-instances/&lt;/td&gt;
&lt;td&gt;12:08PM, Wednesday, September 09 2026, PDT&lt;/td&gt;
&lt;td&gt;2,023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;AWS Lambda vs Step Functions vs EventBridge&lt;/td&gt;
&lt;td&gt;jayendrapatil.com/aws-lambda-vs-step-functions-vs-eventbridge/&lt;/td&gt;
&lt;td&gt;05:00PM, Wednesday, June 24 2026, PDT&lt;/td&gt;
&lt;td&gt;1,363&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full &lt;code&gt;content[0].text&lt;/code&gt; of Web Search (Q1, first run, 9,595 characters) follows. Reading it side by side with Web Answer's answer (627 characters) shows how the citations differ.&lt;/p&gt;

&lt;p&gt;Full response of Web Search (click to expand)&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;"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;"dafbd346"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&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;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"05:00PM, Sunday, October 30 2016, PDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Configure Lambda Event Function timeout Lambda runs your code for a set amount of time before timing out. Timeout is the maximum amount of time in seconds that a Lambda function can run. The default value for this setting is 3 seconds, but you can adjust this in increments of 1 second up to a maximum value &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;of 900 seconds (15 minutes). For functions using AWS Lambda Managed Instances, asynchronous invocations and event source mapping invocations (except Amazon MQ and Amazon DocumentDB) support a maximum allowed value of 5,400 seconds (90 minutes). &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Configuring timeout (AWS SAM) Determining the appropriate timeout value for a Lambda function If the timeout value is close to the average duration of a function, there is a higher risk that the function times out unexpectedly. The duration of a function can vary &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Under General configuration , choose Edit . For Timeout , set a value between 1 and 900 seconds (15 minutes). Choose Save . Configuring timeout (AWS CLI) You can use the update-function-configuration command to configure the timeout value, in seconds. The following example command increases the function timeout to 120 seconds (2 minutes). "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Configure Lambda Event Function timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&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://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html"&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;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The following topics provide troubleshooting advice for common issues that you might encounter related to Lambda function configuration settings. Memory configurations You can configure a Lambda function to use between 128 MB and 10,240 MB of memory. By default, any function created in the console is assigned the smallest amount of memory. Many Lambda functions are performant at this lowest setting. However, if you are importing large code libraries or completing memory intensive tasks, 128 MB is not sufficient. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Timeouts Timeouts for Lambda functions can be set between 1 and 900 seconds (15 minutes). For functions using AWS Lambda Managed Instances, asynchronous invocations and event source mapping invocations (except Amazon MQ and Amazon DocumentDB) support a maximum allowed value of 5,400 seconds (90 minutes). By default, the Lambda console sets this to 3 seconds. The timeout value is a safety valve that ensures functions do not run indefinitely. After the timeout value is reached, Lambda stops the function invocation. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;If a timeout value is set close to the average duration of a function, this increases the risk that the function times out unexpectedly. The duration of a function can vary based on the amount of data transfer and processing, and the latency of any services the function interacts with. Common causes of timeout include: &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;2. Duration – Before the function runs out of memory, it finishes invocations at a steady double-digit millisecond rate. As paging occurs, the duration takes an order of magnitude longer. 3. Error count – As the memory leak exceeds allocated memory, eventually the function errors due to the computation exceeding the timeout, or the execution environment stops the function. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;In JavaScript, asynchronous callbacks are handled with an event loop. Other runtimes use different mechanisms to handle concurrency. When the function's execution environment ends, Lambda freezes the environment until the next invocation. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;1. The code calls the asynchronous doWork function using the await keyword in the handler. 2. The doWork function takes some period of time to complete before resolving the promise. 3. The function times out because doWork takes longer than the timeout limit allows and the callback result is not returned in a later invocation. "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Troubleshoot configuration issues in Lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&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://docs.aws.amazon.com/lambda/latest/operatorguide/configurations.html"&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;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If you agree, AWS and approved third parties will also use cookies to provide useful site features, remember your preferences, and display relevant content, including relevant advertising. To accept or decline all non-essential cookies, choose â€œAcceptâ€ or â€œDecline.â€ To make more detailed choices, choose â€œCustomize.â€ &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;To opt out of our use of cookies or similar technologies to engage in these activities, select â€œOpt out of cross-context behavioral adsâ€ and â€œSave preferencesâ€ below. If you clear your browser cookies or visit this site from a different device or browser, you will need to make your selection again. For more information about cookies and how we use them, read ourÂ Cookie Notice. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;To opt out of the use of other identifiers, such as contact information, for these activities, fill out the formÂ here. For more information about how AWS handles your information, read theÂ AWS Privacy Notice. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;The amount of time (in seconds) that Lambda allows a function to run before stopping it. The default is 3 seconds, and the maximum allowed value is 900 seconds. For functions using Lambda Managed Instances, asynchronous invocations and event source mapping invocations (except Amazon MQ and Amazon DocumentDB) support a maximum allowed value of 5,400 seconds (90 minutes). For more information, see Lambda execution environment. "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&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 SDK for Kotlin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&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://docs.aws.amazon.com/sdk-for-kotlin/api/latest/lambda/aws.sdk.kotlin.services.lambda.model/-update-function-configuration-request/-builder/timeout.html"&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;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12:08PM, Wednesday, September 09 2026, PDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Activating 90-minute function timeout You can now configure any Lambda function running on a Managed Instance with a timeout of up to 90 minutes (5,400 seconds) for async and ESM invocations. Synchronous invocations retain the existing 15-minute maximum. The function executes exactly as before: same runtime, same handler, same IAM execution role, same virtual private cloud (VPC) configuration. The only difference is that your function now supports longer continuous execution. Your initialization code (Init phase) is still limited to 15 minutes on Lambda Managed Instances. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;There is no additional charge for using the 90-minute timeout. Standard Lambda Managed Instances pricing applies. 90-minute timeout and durable functions The 90-minute function timeout and durable functions are complementary. The function timeout (--timeout) controls how long each individual invocation can run, while the durable execution timeout (ExecutionTimeout in --durable-config) controls the total elapsed time from execution start to completion. Durable functions use checkpoints to track progress and automatically recover from failures through replay, re-executing from the beginning while skipping completed work. With today’s launch, each asynchronous invocation in a durable function running on a Managed Instance can now execute for up to 90 minutes continuously, while the corresponding durable execution can run for up to 1 year. For synchronous and event source mapping invocations, both the invocation and the corresponding durable execution are limited to 90 minutes. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Note that invocations for Amazon MQ ESM and Amazon DocumentDB (with MongoDB compatibility) ESM remain limited to 15 minutes. Synchronous invocations (15 minutes maximum): Synchronous invocations retain the existing 15-minute maximum timeout. If you set your function timeout to greater than 15 minutes and invoke it synchronously, Lambda continues to apply the 15-minute timeout. The GetFunctionConfiguration API reports the configured timeout value. "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Announcing 90-minute function timeout on AWS Lambda Managed Instances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&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://aws.amazon.com/blogs/compute/announcing-90-minute-function-timeout-on-aws-lambda-managed-instances/"&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;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"05:00PM, Wednesday, June 24 2026, PDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"| event_date | 2026-08-11 |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| title | AWS Lambda vs Step Functions vs EventBridge |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| original_url | https://jayendrapatil.com/aws-lambda-vs-step-functions-vs-eventbridge/ |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| source_domain | jayendrapatil.com |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| search_queries_hash | 187d207b814cb47441a22825e67d4c83 |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;### search_queries (Level 0)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;#### search_queries - Item 1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- What architecture pattern is best for decoupling microservices that react to a single event? : Amazon EventBridge with a custom event bus and content-based routing rules provides the most decoupled architecture, allowing producers to remain unaware of consumers.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;#### search_queries - Item 2&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- How much does AWS Step Functions Standard Workflow cost? : AWS Step Functions Standard Workflow costs $25.00 per 1 million state transitions.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;#### search_queries - Item 3&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- What is the difference between AWS Lambda, Step Functions, and EventBridge? : Lambda handles compute (executing code), Step Functions handles orchestration (coordinating multiple services into workflows), and EventBridge handles event routing (connecting producers and consumers) — they are complementary services often used together.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;#### search_queries - Item 4&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- What is the maximum execution duration for AWS Lambda? : Standard Lambda functions have a 15-minute timeout, MicroVMs support up to 8 hours, and Lambda Durable Functions can suspend for up to 1 year."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&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 Lambda vs Step Functions vs EventBridge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&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://jayendrapatil.com/aws-lambda-vs-step-functions-vs-eventbridge/"&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;Comparing the two, you can see the following.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Citations are attached differently. Web Answer attaches the source URL to each sentence of the answer (the &lt;code&gt;[[1](...)]&lt;/code&gt; and &lt;code&gt;[[2](...)]&lt;/code&gt; above). Web Search only attaches &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt; to each result, and does not tell you which sentence is based on which result. When you compose an answer, your own model has to add the citations&lt;/li&gt;
&lt;li&gt;The first Web Search snippet already contains the answer (up to 900 seconds, and 5,400 seconds on Managed Instances). However, the answer sits in the middle of explanatory text about how to configure the setting, and extracting it is the job of your own model&lt;/li&gt;
&lt;li&gt;The results also include content that is only weakly related to the answer. The third result contains the text of a cookie consent banner with garbled characters, and the fifth is a list of search queries from a third-party blog, which includes a statement that none of the other four contain: "MicroVMs support up to 8 hours". Deciding which to use is also the job of your own model. Web Answer's answer cited only two sources: the official documentation and an AWS blog&lt;/li&gt;
&lt;li&gt;Web Answer summarized the same content in a 627-character answer and returned it with a citation on each sentence. The numbers in the answer (900 seconds and 5,400 seconds) are the same as those in the first snippet&lt;/li&gt;
&lt;li&gt;The amount returned is 8,227 characters (5 results) for Web Search and 627 characters (Q1) for Web Answer, a difference of about 13 times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference in length comes from the difference in what each tool returns.&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;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it returns&lt;/td&gt;
&lt;td&gt;Excerpts (snippets) of the pages it found, returned without summarizing. Raw material&lt;/td&gt;
&lt;td&gt;Only the answer to the question, written after reading the pages, plus citations. The page bodies are not returned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What determines the length&lt;/td&gt;
&lt;td&gt;The number of results (&lt;code&gt;maxResults&lt;/code&gt;) and the length of each snippet (about 700 to 5,000 characters this time). The difference between questions is small (about 9,600 to 14,100 characters for Q1 to Q3)&lt;/td&gt;
&lt;td&gt;The content of the question. Short for questions that can be answered briefly (about 1,400 to 2,500 characters for Q1 and Q2), and long for questions with a lot to answer (about 10,000 to 12,700 characters for Q3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who extracts the answer&lt;/td&gt;
&lt;td&gt;Your own model&lt;/td&gt;
&lt;td&gt;Web Answer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words, Web Answer was short because the questions could be answered briefly. For Q3 (recent updates to AgentCore), Web Answer's answer was also about 10,000 characters, the same as Web Search's results. The essential difference is not length, but "whether it returns material or an answer". The following diagram summarizes what comes back for the same question (Q1).&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%2Fp19rp5epow1hvcqu4x5y.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%2Fp19rp5epow1hvcqu4x5y.png" alt="Verification diagram (Japanese labels)" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For Q2 (the CET1 ratio under Basel III), Web Answer also answered "a minimum of 4.5%, plus a 2.5% buffer, so effectively 7%", citing three sources. Web Search returned five results, including IMF, BIS, Investopedia, and Wikipedia.&lt;/p&gt;

&lt;p&gt;How to read Web Answer's citations&lt;/p&gt;

&lt;p&gt;According to the official description, marker N corresponds to &lt;code&gt;citations[N-1]&lt;/code&gt; of that block. In my measurements, the same URL appeared multiple times in &lt;code&gt;citations&lt;/code&gt;, so the number of entries was larger than the number of distinct markers (for example, the first run of Q1 had 4 entries but only 2 unique URLs). Looking up by &lt;code&gt;citations[N-1]&lt;/code&gt; mismatched the marker's URL in 7 of 9 runs, while looking up by the Nth URL in order of first appearance after removing duplicates matched in all 9 runs.&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;re&lt;/span&gt;

&lt;span class="n"&gt;PAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[\[(\d+)\]\((https?://[^)]+)\)\]&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;unique_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;citations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seen&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;citations&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;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&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="n"&gt;seen&lt;/span&gt;
&lt;span class="c1"&gt;# URL of marker N = unique_urls(citations)[N-1]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the sample is only 3 questions × 3 runs, in us-east-1, on a single day. There is no guarantee that this mapping always holds, and it may change with specification changes. If you process citations programmatically, it is safer to use the URL in the marker itself.&lt;/p&gt;

&lt;p&gt;The results of Verification 1 affect the choice as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want to reason over the results with your own model, Web Search is faster (about 1.1 seconds versus about 3.1 seconds for Q1), and &lt;code&gt;maxResults&lt;/code&gt; lets you limit the number of results. However, dates have string variations and &lt;code&gt;unknown&lt;/code&gt; is mixed in, so if you use dates as a filtering condition, it is safer to use the request-side &lt;code&gt;publishedDateFilter&lt;/code&gt; (Verification 4)&lt;/li&gt;
&lt;li&gt;If you want a finished answer, Web Answer returns it with citations, but it took about 6.6 seconds for the latest-information question (Q3). If latency matters, measure by question type before choosing&lt;/li&gt;
&lt;li&gt;When you use Web Answer's citations for display, use the URL in the marker, not the index into &lt;code&gt;citations&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verification 2: Separation of IAM responsibilities
&lt;/h3&gt;

&lt;p&gt;To establish the minimum required permissions, I checked the behavior with the service role's &lt;code&gt;InvokeWebAnswer&lt;/code&gt; / &lt;code&gt;InvokeWebSearch&lt;/code&gt; present and absent, and with &lt;code&gt;InvokeGateway&lt;/code&gt; attached to different roles.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2-1 Baseline: service role has only the dedicated permissions, caller has &lt;code&gt;InvokeGateway&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-2 Remove &lt;code&gt;InvokeWebAnswer&lt;/code&gt; from the service role&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Failure (HTTP 200, JSON-RPC error &lt;code&gt;-32002&lt;/code&gt;: &lt;code&gt;Execution role is not authorized for connector web-answer&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-3 Restore &lt;code&gt;InvokeWebAnswer&lt;/code&gt; and remove &lt;code&gt;InvokeWebSearch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Failure (HTTP 200, JSON-RPC error &lt;code&gt;-32002&lt;/code&gt;: &lt;code&gt;Execution role is not authorized for connector web-search&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-4 Add &lt;code&gt;InvokeGateway&lt;/code&gt; to the service role (with both dedicated permissions)&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-5 Remove &lt;code&gt;InvokeGateway&lt;/code&gt; from the caller (service role as in 2-4)&lt;/td&gt;
&lt;td&gt;Failure (HTTP 403, JSON-RPC error &lt;code&gt;-32002&lt;/code&gt;: &lt;code&gt;Authorization error - Insufficient permissions&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Failure (same as left)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I made the calls by assuming a dedicated verification IAM role (&lt;code&gt;wa-verify-caller&lt;/code&gt;) with &lt;code&gt;assume-role&lt;/code&gt;. After each permission change I waited 20 seconds before calling, and every configuration gave the expected result from the first call (no retries were needed to wait for propagation).&lt;/p&gt;

&lt;p&gt;The results of Verification 2 show the following.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;InvokeGateway&lt;/code&gt; is required on the caller and not on the service role (in 2-1 it succeeded without it on the service role; in 2-4 adding it changed nothing; in 2-5 it failed without it on the caller)&lt;/li&gt;
&lt;li&gt;The dedicated permissions on the service role are independent per tool. Removing only Web Answer's permission left Web Search working, and vice versa. You can remove the permissions for tools you don't use from the service role&lt;/li&gt;
&lt;li&gt;Errors appear in three different ways depending on the cause. This can also help you narrow down a permission problem&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;Cause&lt;/th&gt;
&lt;th&gt;HTTP status&lt;/th&gt;
&lt;th&gt;How it is returned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The service role lacks the tool's permission&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;JSON-RPC &lt;code&gt;error&lt;/code&gt; (&lt;code&gt;-32002&lt;/code&gt;, &lt;code&gt;Execution role is not authorized for connector ...&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The caller lacks &lt;code&gt;InvokeGateway&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;JSON-RPC &lt;code&gt;error&lt;/code&gt; (&lt;code&gt;-32002&lt;/code&gt;, &lt;code&gt;Authorization error - Insufficient permissions&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid filter or date specification (Verifications 3 and 4)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isError: true&lt;/code&gt; inside &lt;code&gt;result&lt;/code&gt; (&lt;code&gt;DependencyFailedException&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Caller code will miss failures unless it checks all three places: the HTTP status, the JSON-RPC &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;result.isError&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification 3: Combination rules of the domain filters
&lt;/h3&gt;

&lt;p&gt;To check the specification that "the agent cannot widen the boundary", I varied the combinations of include and exclude on the administrator side and the request side, and checked the results and errors.&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;Administrator side&lt;/th&gt;
&lt;th&gt;Request side&lt;/th&gt;
&lt;th&gt;Expected (official)&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty intersection&lt;/td&gt;
&lt;td&gt;include: docs.aws.amazon.com&lt;/td&gt;
&lt;td&gt;include: bis.org&lt;/td&gt;
&lt;td&gt;Web Answer: validation error&lt;/td&gt;
&lt;td&gt;Both tools returned an error. Answer: &lt;code&gt;include contains domains that are not permitted for this tool: bis.org&lt;/code&gt;. Search: &lt;code&gt;Runtime domainFilter.include has no overlap with admin domainFilter.include&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixing in a domain outside the boundary&lt;/td&gt;
&lt;td&gt;include: docs.aws.amazon.com&lt;/td&gt;
&lt;td&gt;include: docs.aws.amazon.com, wikipedia.org&lt;/td&gt;
&lt;td&gt;Domains outside the boundary are not used&lt;/td&gt;
&lt;td&gt;Search: no error, and all 5 results were from docs.aws.amazon.com (only the intersection was used). Answer: error (rejected because &lt;code&gt;wikipedia.org&lt;/code&gt; is not permitted; it fails even when there is an intersection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merging excludes&lt;/td&gt;
&lt;td&gt;exclude: investopedia.com&lt;/td&gt;
&lt;td&gt;exclude: imf.org&lt;/td&gt;
&lt;td&gt;Both excluded&lt;/td&gt;
&lt;td&gt;The two domains that had appeared in the baseline (Q2 of V1) no longer appeared in either tool (Search: bis.org, fraser.stlouisfed.org, debevoise.com, en.wikipedia.org, and others. Answer: bis.org, fraser.stlouisfed.org, mayerbrown.com)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can the administrator's exclude be bypassed?&lt;/td&gt;
&lt;td&gt;exclude: investopedia.com&lt;/td&gt;
&lt;td&gt;include: investopedia.com&lt;/td&gt;
&lt;td&gt;Cannot be overridden&lt;/td&gt;
&lt;td&gt;Answer: error (&lt;code&gt;include contains domains that are excluded, fully or partly, for this tool: investopedia.com. Remove them from include.&lt;/code&gt;). Search: HTTP 200, &lt;code&gt;isError: false&lt;/code&gt;, 0 results (an empty result, not an error)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Errors caused by filters were returned with HTTP status 200, &lt;code&gt;isError: true&lt;/code&gt;, and &lt;code&gt;DependencyFailedException - ...&lt;/code&gt; in the body. You cannot detect the failure from the HTTP status alone.&lt;/p&gt;

&lt;p&gt;Verification 3 showed three things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The boundary set by the administrator was actually respected by both tools. Domains outside the boundary and domains the administrator excluded did not appear in the results (one observation per pattern)&lt;/li&gt;
&lt;li&gt;The way a violation looks is different. Web Answer reports an error if even one non-permitted or excluded domain is included in the request. Web Search quietly narrows down to the intersection, and when the request collides with an exclude, it returns an empty result without an error&lt;/li&gt;
&lt;li&gt;An agent that uses Web Search may interpret an empty result as "nothing was found". The calling side needs to decide how to handle zero results&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verification 4: Date filter
&lt;/h3&gt;

&lt;p&gt;To see whether the date filter can be used for freshness requirements, I varied the range of &lt;code&gt;publishedDateFilter&lt;/code&gt; and checked whether the results changed. I also checked the error when &lt;code&gt;from&lt;/code&gt; is later than &lt;code&gt;to&lt;/code&gt;.&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;Web Search&lt;/th&gt;
&lt;th&gt;Web Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Limit to the last month (&lt;code&gt;from: 2026-10-08&lt;/code&gt;, &lt;code&gt;to: 2026-11-08&lt;/code&gt;, question Q3)&lt;/td&gt;
&lt;td&gt;All 10 results were in range (&lt;code&gt;publishedDate&lt;/code&gt; from 2026-10-11 to 2026-11-06). Before filtering (V1), results from April to September 2026 were mixed in&lt;/td&gt;
&lt;td&gt;The cited sources were mainly pages from October to November 2026 (the Web Answer announcement page, October AgentCore announcements, and so on). Because the dates of the cited sources are not attached to the result, you can only judge whether they are within the range from the text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;from&lt;/code&gt; &amp;gt; &lt;code&gt;to&lt;/code&gt; (&lt;code&gt;from: 2026-11-08&lt;/code&gt;, &lt;code&gt;to: 2026-10-08&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Error: &lt;code&gt;publishedDate.from (2026-11-08) must not be after publishedDate.to (2026-10-08)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Error: &lt;code&gt;filters.publishedDateFilter.from must not be after to.&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Verification 4 shows how to use it for freshness requirements.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For use cases that need freshness, both can be narrowed with the request-side &lt;code&gt;publishedDateFilter&lt;/code&gt;. However, only Web Search lets you check the dates of the results yourself (Web Answer's citations carry no dates)&lt;/li&gt;
&lt;li&gt;Errors in the date filter are also returned with HTTP 200 and &lt;code&gt;isError: true&lt;/code&gt; (same as Verification 3)&lt;/li&gt;
&lt;li&gt;Web Search's &lt;code&gt;publishedDate&lt;/code&gt; was a string containing &lt;code&gt;PDT&lt;/code&gt; or &lt;code&gt;PST&lt;/code&gt;. If you want to sort by date, you need to parse it yourself&lt;/li&gt;
&lt;li&gt;Web Answer is given today's date when generating an answer, and this answer said "current date: November 7, 2026" (apparently the date in Pacific Time)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Criteria for choosing
&lt;/h2&gt;

&lt;p&gt;Based on the results of Verifications 1 to 4, here is a summary by criterion. The "Basis" column shows which verification in this article it rests on.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Choose Web Search&lt;/th&gt;
&lt;th&gt;Choose Web Answer&lt;/th&gt;
&lt;th&gt;Basis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You want to reason over the returned results with your own model&lt;/td&gt;
&lt;td&gt;Suitable (url, title, snippet, and date are returned)&lt;/td&gt;
&lt;td&gt;Not suitable (a finished answer is returned)&lt;/td&gt;
&lt;td&gt;Verification 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want a finished answer to a sub-question&lt;/td&gt;
&lt;td&gt;You need to synthesize it yourself&lt;/td&gt;
&lt;td&gt;Suitable (returned with citations)&lt;/td&gt;
&lt;td&gt;Verification 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response speed matters&lt;/td&gt;
&lt;td&gt;Fast (average about 1.1 to 1.3 seconds)&lt;/td&gt;
&lt;td&gt;Varies by question (about 3.0 seconds; about 6.6 seconds for latest-information questions)&lt;/td&gt;
&lt;td&gt;Verification 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want to keep everything within the Tokyo Region&lt;/td&gt;
&lt;td&gt;Suitable&lt;/td&gt;
&lt;td&gt;Not possible as of 2026-11-08 (may change)&lt;/td&gt;
&lt;td&gt;Region table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want to pin the referenced domains on the administrator side&lt;/td&gt;
&lt;td&gt;Possible with both. Outside the boundary, results are narrowed or empty&lt;/td&gt;
&lt;td&gt;Possible with both. Specifying outside the boundary returns an error&lt;/td&gt;
&lt;td&gt;Verification 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want failures to be easy to notice&lt;/td&gt;
&lt;td&gt;An empty result can be returned without an error&lt;/td&gt;
&lt;td&gt;Invalid specifications are returned as errors&lt;/td&gt;
&lt;td&gt;Verification 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want to filter by date&lt;/td&gt;
&lt;td&gt;Possible. You can check the result's &lt;code&gt;publishedDate&lt;/code&gt; (custom format)&lt;/td&gt;
&lt;td&gt;Possible. The dates of cited sources are not attached to the result&lt;/td&gt;
&lt;td&gt;Verification 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want to guarantee citations are displayed in a machine-checkable way&lt;/td&gt;
&lt;td&gt;Display the result's url yourself&lt;/td&gt;
&lt;td&gt;The answer comes back with citations. Use the URL in the marker&lt;/td&gt;
&lt;td&gt;Verification 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is a rough guide for choosing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want to control how the results are handled by your own model, or you want to use Tokyo, choose Web Search&lt;/li&gt;
&lt;li&gt;If you want to use the finished answer and citations as they are, and us-east-1 is acceptable, choose Web Answer&lt;/li&gt;
&lt;li&gt;You can also use both. AWS also calls them "complementary", and you can split by question type, for example Web Answer for fact checks and Web Search for preliminary research&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As for citations, for both tools it is a condition of use to keep and display the cited sources and links. Bulk extraction and building a competing index are prohibited.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify it yourself
&lt;/h2&gt;

&lt;p&gt;This shows a flow that readers can try in their own environment. The following is a shortened version of the code I actually used in the verification. I did the verification with a dedicated role and Gateway, and deleted everything after finishing.&lt;/p&gt;

&lt;p&gt;You need AWS CLI v2, Python 3.10 or later, boto3 (1.43.91 at the time of verification), and requests. The Region is &lt;code&gt;us-east-1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the service role
&lt;/h3&gt;

&lt;p&gt;The trust policy (&lt;code&gt;trust.json&lt;/code&gt;). Replace &lt;code&gt;&amp;lt;ACCOUNT_ID&amp;gt;&lt;/code&gt; with your own account ID.&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;"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="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;"bedrock-agentcore.amazonaws.com"&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;"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="nl"&gt;"aws:SourceAccount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;ACCOUNT_ID&amp;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;"ArnLike"&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;"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:bedrock-agentcore:us-east-1:&amp;lt;ACCOUNT_ID&amp;gt;:gateway/*"&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;The permissions policy (&lt;code&gt;policy.json&lt;/code&gt;). It is required independently per tool.&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;"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="s2"&gt;"bedrock-agentcore:InvokeWebAnswer"&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:bedrock-agentcore:us-east-1:aws:tool/web-answer.v1"&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="s2"&gt;"bedrock-agentcore:InvokeWebSearch"&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:bedrock-agentcore:us-east-1:aws:tool/web-search.v1"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; wa-verify-gateway-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; file://trust.json
aws iam put-role-policy &lt;span class="nt"&gt;--role-name&lt;/span&gt; wa-verify-gateway-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-name&lt;/span&gt; wa-verify-base &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inline policy of the created role looks like this in the console.&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%2Frr7cci8ismqxtivd95hw.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%2Frr7cci8ismqxtivd95hw.png" alt="Inline policy of the role in the console" width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create the Gateway and targets with Python
&lt;/h3&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;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;control&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore-control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_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;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_gateway&lt;/span&gt;&lt;span class="p"&gt;(&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;wa-verify-gateway&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;roleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::&amp;lt;ACCOUNT_ID&amp;gt;:role/wa-verify-gateway-role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;protocolType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;authorizerType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_IAM&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="n"&gt;gid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gatewayId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_gateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;READY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&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;add_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connector_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_gateway_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;targetConfiguration&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;mcp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connector&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connectorId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;connector_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;configurations&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;config_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameterValues&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="p"&gt;}}},&lt;/span&gt;
        &lt;span class="n"&gt;credentialProviderConfigurations&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;credentialProviderType&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;GATEWAY_IAM_ROLE&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;while&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_gateway_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targetId&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;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;READY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;add_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web-answer-tool&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;web-answer&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;WebAnswer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;add_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web-search-tool&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;web-search&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;WebSearch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right after creating the role, it is safer to wait about 10 seconds for propagation before creating the Gateway. If you do not specify a version, the current default is used (at the time of verification, 1.1.0 for Web Answer and 1.3.0 for Web Search).&lt;/p&gt;

&lt;p&gt;The Gateway reached the Ready status about 10 seconds after creation. Both targets also became Ready within a few seconds.&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%2Fykadl8jdcbx7d35sfp8l.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%2Fykadl8jdcbx7d35sfp8l.png" alt="Gateway in Ready status" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the target list, the target types are displayed as "ウェブ回答ツール" (Web Answer tool) and "ウェブ検索ツール" (Web Search tool).&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%2F6mntowzdck4yyzbsl1x8.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%2F6mntowzdck4yyzbsl1x8.png" alt="Targets in Ready status" width="799" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings screens in the console
&lt;/h3&gt;

&lt;p&gt;You can make the same settings from the console. In step 3, "Add target", of the Gateway creation wizard, choose as follows. The console in the screenshots is displayed in Japanese, so the Japanese labels are shown in parentheses.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave the target protocol as "MCP target" (MCP ターゲット)&lt;/li&gt;
&lt;li&gt;For the target type, choose "Connector - New" (コネクター - 新規). The other options are MCP server, Lambda ARN, REST API, and API Gateway&lt;/li&gt;
&lt;li&gt;"Pre-configured targets" (事前設定済みのターゲット) lists knowledge base (KB), Web Search tool (ウェブ検索ツール), Web Answer tool (ウェブ回答ツール), Step Functions, and other integrations. Choose "Web Answer tool" here&lt;/li&gt;
&lt;li&gt;When you choose "Web Answer tool", a blue notice appears. It says what the developer guide says: because it uses geographic cross-Region inference, data processing stays in commercial Regions within the specified geographic boundary (such as the US, EU, or APAC)&lt;/li&gt;
&lt;li&gt;For the outbound authentication setting, only "IAM role" (IAM ロール) can be selected (OAuth client, API key, and no authorization cannot be selected)&lt;/li&gt;
&lt;/ol&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%2Fs0v1tkly1yzqetjrtkpi.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%2Fs0v1tkly1yzqetjrtkpi.png" alt="Web Answer tool configuration screen in the console" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The correspondence with the code is as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Console item&lt;/th&gt;
&lt;th&gt;Code item&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target type "Connector", pre-configured target "Web Answer tool"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connector.source.connectorId: "web-answer"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-configured target "Web Search tool"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connectorId: "web-search"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound authentication setting "IAM role"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;credentialProviderType: "GATEWAY_IAM_ROLE"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Domain filtering" (ドメインフィルタリング) under additional settings (included / excluded)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;parameterValues.domainFilter.include&lt;/code&gt; / &lt;code&gt;exclude&lt;/code&gt; (the "administrator-side" boundary in Verification 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you open "Additional settings - Optional" (追加設定 - オプション), there is a "Domain filtering" item. Its description is "Set the domains to include in and exclude from this target", the current counts are shown as "Included: 0 · Excluded: 0", and you edit them with the "View and manage domains" button. In other words, the boundary that the administrator sets on the target, which Verification 3 dealt with, is set in the target's additional settings in the console.&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%2Fgi3m26cbd6qzu67dqbjc.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%2Fgi3m26cbd6qzu67dqbjc.png" alt="Additional settings of the Web Answer tool in the console" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On this screen (with additional settings opened), I did not see an item for choosing the connector version. If you want to pin the version, use &lt;code&gt;source.version&lt;/code&gt;, as in the code in this article (I did not check whether it can be pinned in the console).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Call MCP with SigV4 signing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;tools/call&lt;/code&gt; worked without &lt;code&gt;initialize&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SigV4Auth&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.awsrequest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AWSRequest&lt;/span&gt;

&lt;span class="n"&gt;REGION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;GATEWAY_ID&amp;gt;.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;GATEWAY_ID&amp;gt; is the gatewayId returned by create_gateway (e.g. name-alphanumerics)
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&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;2.0&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}})&lt;/span&gt;
    &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get_credentials&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_frozen_credentials&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AWSRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;headers&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;Content-Type&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;application/json&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;Accept&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;application/json, text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nc"&gt;SigV4Auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;add_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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="n"&gt;text&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/list&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;web-answer-tool___WebAnswer&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;arguments&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the maximum execution timeout of an AWS Lambda function?&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;web-search-tool___WebSearch&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;arguments&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the maximum execution timeout of an AWS Lambda function?&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;maxResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller's IAM needs &lt;code&gt;bedrock-agentcore:InvokeGateway&lt;/code&gt; (targeting the Gateway ARN). Check for failures in three places: the HTTP status, the JSON-RPC &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;result.isError&lt;/code&gt; (see Verifications 2 and 3).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Try the domain filters (optional)
&lt;/h3&gt;

&lt;p&gt;Change the administrator-side boundary with &lt;code&gt;update_gateway_target&lt;/code&gt;. The required arguments are &lt;code&gt;gatewayIdentifier&lt;/code&gt;, &lt;code&gt;targetId&lt;/code&gt;, and &lt;code&gt;targetConfiguration&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;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_gateway_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;targetId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;TARGET_ID&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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;web-search-tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;targetConfiguration&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;mcp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connector&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connectorId&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;web-search&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;configurations&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;WebSearch&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;parameterValues&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domainFilter&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs.aws.amazon.com&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="p"&gt;}}},&lt;/span&gt;
    &lt;span class="n"&gt;credentialProviderConfigurations&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;credentialProviderType&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;GATEWAY_IAM_ROLE&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the request side, add &lt;code&gt;filters&lt;/code&gt; to &lt;code&gt;arguments&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="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="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filters&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domainFilter&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bis.org&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="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="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filters&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publishedDateFilter&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&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;2026-10-08&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;to&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;2026-11-08&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Clean up
&lt;/h3&gt;

&lt;p&gt;Delete the targets, the Gateway, and the IAM role, in that order.&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;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_gateway_targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_gateway_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# Wait for the target deletions to complete
&lt;/span&gt;&lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_gateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam delete-role-policy &lt;span class="nt"&gt;--role-name&lt;/span&gt; wa-verify-gateway-role &lt;span class="nt"&gt;--policy-name&lt;/span&gt; wa-verify-base
aws iam delete-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; wa-verify-gateway-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;I started out with the understanding that Web Answer is "something where a model reads web search results and answers for you". After actually running both and comparing them, I found that the difference, where Web Search returns material in the form of search results and Web Answer returns an answer with citations, becomes the axis for choosing between them, and it even affects how errors look and which Regions you can use. If you want to use a finished answer and its citations as they are, choose Web Answer. If you want to handle the results with your own model, choose Web Search.&lt;br&gt;
As of 2026-11-08, Web Answer is available only in us-east-1, but the blog says more Regions are planned. Connector versions and tool names may also change in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/11/web-answer-amazon-bedrock/" rel="noopener noreferrer"&gt;Announcing Web Answer on Amazon Bedrock AgentCore (2026-11-05)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-web-answer-on-amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;Getting started with Web Answer on Amazon Bedrock AgentCore (AWS Machine Learning Blog, 2026-11-05)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-answer-tool.html" rel="noopener noreferrer"&gt;Web Answer (AgentCore Developer Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Web Search Tool (AgentCore Developer Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-versions.html" rel="noopener noreferrer"&gt;Connector versions (AgentCore Developer Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-add-target-api-target-config.html" rel="noopener noreferrer"&gt;Gateway target configuration (AgentCore Developer Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/grounding/web-answer/" rel="noopener noreferrer"&gt;Web Answer (product page)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/grounding/web-search/" rel="noopener noreferrer"&gt;Web Search (product page)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/agentcore/pricing/" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>agentcore</category>
      <category>mcp</category>
      <category>amazonbedrock</category>
    </item>
    <item>
      <title>From ETL Jobs to Agentic Orchestrators</title>
      <dc:creator>kienly</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:42:29 +0000</pubDate>
      <link>https://dev.to/aws-builders/from-etl-jobs-to-agentic-orchestrators-3332</link>
      <guid>https://dev.to/aws-builders/from-etl-jobs-to-agentic-orchestrators-3332</guid>
      <description>&lt;p&gt;I have spent most of my career on the boring, important part of data work: the pipelines that run at 2 a.m. and that nobody thinks about until they break. And I have always liked pipelines for one simple reason. They do not improvise. Give a workflow engine a schedule, a schema, and a known sequence of steps, and it will run the same plan every single day, without getting tired or distracted or "creative."&lt;/p&gt;

&lt;p&gt;That predictability is the whole point. It is also the reason I was skeptical when people started talking about putting AI agents in charge of data operations.&lt;/p&gt;

&lt;p&gt;This post is about where I landed. Short version: an agent is a great front door to a pipeline, but it should never be the thing that decides how the pipeline runs. The pipeline should keep that job. Here is how to set it up that way with AWS Step Functions and Amazon Bedrock AgentCore Gateway.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the boring part is still the hard part
&lt;/h2&gt;

&lt;p&gt;Pipelines are predictable on paper. In practice, the operational work around them is anything but.&lt;/p&gt;

&lt;p&gt;Late files arrive in Amazon S3 hours after the batch window closed. An API throttles during a busy week. Someone in finance messages the on-call channel: "Can you re-run yesterday's partition? We fixed a reference table." The pipeline itself is automated, but the control plane around it is still manual. Someone reads the message, checks the S3 key, opens the console or the CLI, builds a JSON payload, starts the run, watches it, and reports back.&lt;/p&gt;

&lt;p&gt;It is repetitive, low-risk work that eats time. So it is natural to want an AI agent to handle it.&lt;/p&gt;

&lt;p&gt;The trap is what that usually looks like. The first instinct is to hand the agent the keys to the cloud. Give it permission to call &lt;code&gt;glue:StartJobRun&lt;/code&gt;, &lt;code&gt;redshift-data:ExecuteStatement&lt;/code&gt;, &lt;code&gt;lambda:InvokeFunction&lt;/code&gt;, and &lt;code&gt;s3:CopyObject&lt;/code&gt;, and you have quietly asked a language model to act as a distributed transaction coordinator. I would not trust a person to do that job from a chat window at midnight, and I do not think a model should either. If the agent gets one parameter wrong on step three of a five-step load, or times out while a Spark cluster is starting, or retries a load that was not idempotent, you are left with a half-written table and no single place that says what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better split: the agent asks, the workflow does
&lt;/h2&gt;

&lt;p&gt;AWS recently made &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/step-functions-workflows-ai-agent-tools/" rel="noopener noreferrer"&gt;Step Functions workflows available as tools for AI agents&lt;/a&gt; through Amazon Bedrock AgentCore Gateway, using the Model Context Protocol (MCP). It was announced alongside OpenAI GPT-6.1 Terra on Bedrock, which is covered in the &lt;a href="https://aws.amazon.com/blogs/aws/aws-weekly-roundup-gpt-6-1-terra-on-bedrock-step-functions-as-agent-tools-and-more-november-2-2026/" rel="noopener noreferrer"&gt;AWS Weekly Roundup&lt;/a&gt;. The useful part for us: you can expose a workflow you already trust as a set of MCP tools, without writing a wrapper Lambda or any glue code.&lt;/p&gt;

&lt;p&gt;The design rule I keep coming back to is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The agent decides when to act and with what input. Step Functions decides how the workflow runs.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else in this post follows from that split.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The architecture, in one picture
&lt;/h2&gt;

&lt;p&gt;First, a quick note on MCP. It is an open protocol, built on JSON-RPC, that lets an AI application discover tools (&lt;code&gt;tools/list&lt;/code&gt;) and call them (&lt;code&gt;tools/call&lt;/code&gt;). The appeal is that you expose a tool server once, and any compliant agent can use it, without custom adapters for every model or framework.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-step-functions.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Gateway&lt;/a&gt; acts as that managed MCP boundary. It ships with a built-in Step Functions connector, &lt;code&gt;connectorId: "states"&lt;/code&gt;. When you register a Step Functions target, the Gateway generates MCP-compliant schemas for the state machine operations you choose, and it calls Step Functions with its own IAM service role.&lt;/p&gt;

&lt;p&gt;Here is the whole flow:&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%2F4r0pexcxpip81pn3968p.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%2F4r0pexcxpip81pn3968p.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each layer has a narrow job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The operator and the agent.&lt;/strong&gt; You ask in plain language. The agent, running on a Bedrock model such as GPT-6.1 Terra, turns that into structured parameters, picks the right tool, checks the status, and tells you what happened. It is good at the translation. It is not in charge of the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AgentCore Gateway.&lt;/strong&gt; This is the protocol and security boundary. It exposes the Step Functions operations you selected, generates their input schemas, injects the values you pinned (such as &lt;code&gt;stateMachineArn&lt;/code&gt;), and authenticates with a tightly scoped role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step Functions.&lt;/strong&gt; This is where the real work is decided: state transitions, payload validation, error handling, retries with backoff, and failure notifications. The workflow is the source of truth for what happens next. Step Functions also integrates with a long list of AWS services, so the orchestration logic does not have to be rewritten to become agent-accessible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data services.&lt;/strong&gt; S3, Glue, DynamoDB and SNS do the heavy lifting: storage, Spark processing, audit records, and alerts. They only accept requests from the Step Functions execution role. The agent never talks to them directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this beats giving the agent direct access
&lt;/h3&gt;

&lt;p&gt;Compare that with handing an agent tool definitions for &lt;code&gt;s3:HeadObject&lt;/code&gt;, &lt;code&gt;glue:StartJobRun&lt;/code&gt;, &lt;code&gt;redshift-data:ExecuteStatement&lt;/code&gt;, and &lt;code&gt;sns:Publish&lt;/code&gt;. In that setup, the plan for the pipeline lives inside the model's context window. If the agent process dies while a 20-minute Glue job is running, nothing starts the downstream load or sends the failure alert. An agent with direct Glue or warehouse permissions could also be talked into running something it should not, through prompt injection hidden in a ticket or a filename.&lt;/p&gt;

&lt;p&gt;With Step Functions in the middle, the agent's reach shrinks to one entry point: the workflow. It cannot reorder the steps, skip validation, turn off alerts, or quietly widen its own permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The scenario: a corrected sales extract at an odd hour
&lt;/h2&gt;

&lt;p&gt;Let's make this concrete. An upstream e-commerce platform uploads a corrected daily sales file to S3, well outside the midnight batch. An engineer (or a Slack ops bot) asks the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Start the sales ingestion pipeline for &lt;code&gt;raw/sales/2026-11-05/sales.parquet&lt;/code&gt; and tell me if it succeeds."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No shell scripts, no console clicking. The agent works through four steps, using the &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-agentcore-gateway.html" rel="noopener noreferrer"&gt;AWS Step Functions and AgentCore Gateway integration&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discover the tools and pull out the input.&lt;/strong&gt; The agent lists the MCP tools on the Gateway target, finds &lt;code&gt;StartExecution&lt;/code&gt;, and puts &lt;code&gt;raw/sales/2026-11-05/sales.parquet&lt;/code&gt; into the &lt;code&gt;sourceKey&lt;/code&gt; field. It never needs the state machine ARN, because that is pinned in the Gateway configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the workflow.&lt;/strong&gt; The Gateway calls &lt;code&gt;states:StartExecution&lt;/code&gt; on the pinned Standard workflow. The run goes through these states:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ValidateInput&lt;/code&gt;: a &lt;code&gt;Choice&lt;/code&gt; state that checks that &lt;code&gt;sourceKey&lt;/code&gt; is present and matches &lt;code&gt;raw/sales/*.parquet&lt;/code&gt; before any compute is started.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;StartGlueJob&lt;/code&gt;: a synchronous task (&lt;code&gt;arn:aws:states:::glue:startJobRun.sync&lt;/code&gt;) that starts the PySpark job with the source key and waits for it to finish. Transient failures are retried with exponential backoff.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RecordSuccess&lt;/code&gt;: writes an audit record (execution ARN, source key, completion time, status) to a DynamoDB table.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NotifyFailure&lt;/code&gt; and &lt;code&gt;WorkflowFailed&lt;/code&gt;: if validation or the Glue job fails after its retries, a &lt;code&gt;Catch&lt;/code&gt; routes to an SNS notification and ends in an explicit &lt;code&gt;Fail&lt;/code&gt; state.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch it.&lt;/strong&gt; When &lt;code&gt;StartExecution&lt;/code&gt; returns an &lt;code&gt;executionArn&lt;/code&gt;, the agent calls &lt;code&gt;DescribeExecution&lt;/code&gt; until the status leaves &lt;code&gt;RUNNING&lt;/code&gt;. If something fails, or you want the details, it calls &lt;code&gt;GetExecutionHistory&lt;/code&gt; and points to the exact state where things went wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarize.&lt;/strong&gt; Once the run is finished, the agent turns the status, duration, and audit record into a short, readable answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Standard workflows, not Express
&lt;/h3&gt;

&lt;p&gt;Step Functions has two workflow types. For agent-driven pipelines, I would use &lt;strong&gt;Standard&lt;/strong&gt; every time, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runs can be long.&lt;/strong&gt; Glue and Redshift jobs often take longer than the five-minute limit on Express workflows. Standard workflows can run for up to a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need the history.&lt;/strong&gt; Standard workflows keep a full execution history that &lt;code&gt;DescribeExecution&lt;/code&gt; and &lt;code&gt;GetExecutionHistory&lt;/code&gt; can query. Express workflows send logs to CloudWatch asynchronously and do not support these calls in the same way, which is a problem when the agent needs to answer "what happened?"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The implementation, piece by piece
&lt;/h2&gt;

&lt;p&gt;The repository is deployed with AWS SAM (Serverless Application Model). The full, copy-paste version lives in the &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt;. Four design choices took me the most time to get right, and they are the ones I would look at first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The state machine: validate before you spend money
&lt;/h3&gt;

&lt;p&gt;The first defense against a bad agent call is an explicit &lt;code&gt;ValidateInput&lt;/code&gt; state in the Amazon States Language definition (&lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/state-machine/data-pipeline.asl.json" rel="noopener noreferrer"&gt;&lt;code&gt;state-machine/data-pipeline.asl.json&lt;/code&gt;&lt;/a&gt;). &lt;code&gt;StartExecution&lt;/code&gt; takes its &lt;code&gt;input&lt;/code&gt; as a serialized JSON string, and the Gateway's schema cannot check the contents of that string. So the workflow has to. The &lt;code&gt;Choice&lt;/code&gt; state below rejects bad input right away, which costs next to nothing compared with starting a Glue job:&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;"ValidateInput"&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;"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;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ensure sourceKey is provided and targets the raw/sales/ prefix with a .parquet extension"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&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;"And"&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;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.sourceKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IsPresent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.sourceKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IsString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.sourceKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StringMatches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"raw/sales/*.parquet"&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;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StartGlueJob"&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;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FormatValidationFailure"&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;StartGlueJob&lt;/code&gt; uses the &lt;code&gt;.sync&lt;/code&gt; integration pattern (&lt;code&gt;arn:aws:states:::glue:startJobRun.sync&lt;/code&gt;). Step Functions starts the job, waits, and only moves to &lt;code&gt;RecordSuccess&lt;/code&gt; once Spark has finished. You do not write any polling code.&lt;/p&gt;

&lt;p&gt;One detail that has bitten people, including me in early drafts: if you route the error path to &lt;code&gt;NotifyFailure&lt;/code&gt; and that state ends with &lt;code&gt;"End": true&lt;/code&gt;, Step Functions considers the error handled and marks the whole execution &lt;code&gt;SUCCEEDED&lt;/code&gt;. The agent then reports a success that never happened. The fix is to send the error path to a &lt;code&gt;"Type": "Fail"&lt;/code&gt; state.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Gateway target: pin what the agent should never choose
&lt;/h3&gt;

&lt;p&gt;The Gateway target is defined in &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/gateway/target.json" rel="noopener noreferrer"&gt;&lt;code&gt;gateway/target.json&lt;/code&gt;&lt;/a&gt;. It sets &lt;code&gt;"connectorId": "states"&lt;/code&gt; and lists the operations to expose. The important part is pinning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State-machine operations&lt;/strong&gt; (&lt;code&gt;StartExecution&lt;/code&gt;, &lt;code&gt;ListExecutions&lt;/code&gt;, &lt;code&gt;DescribeStateMachine&lt;/code&gt;) work on a &lt;code&gt;stateMachineArn&lt;/code&gt;. We pin that value in the target. The Gateway removes the parameter from the schema the agent sees and fills it in at call time. The agent has no way to pick a different state machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution operations&lt;/strong&gt; (&lt;code&gt;DescribeExecution&lt;/code&gt;, &lt;code&gt;GetExecutionHistory&lt;/code&gt;, &lt;code&gt;StopExecution&lt;/code&gt;) work on an &lt;code&gt;executionArn&lt;/code&gt;, which is different for every run. It cannot be pinned, so the agent passes the ARN it got back from &lt;code&gt;StartExecution&lt;/code&gt;. Access is controlled by IAM resource scoping instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Two tiers of IAM, because Step Functions has two ARN formats
&lt;/h3&gt;

&lt;p&gt;This one trips people up. State-machine actions and execution actions are checked against different resource ARNs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;State Machine ARN format:
arn:aws:states:&amp;lt;region&amp;gt;:&amp;lt;account-id&amp;gt;:stateMachine:&amp;lt;state-machine-name&amp;gt;

Execution ARN format:
arn:aws:states:&amp;lt;region&amp;gt;:&amp;lt;account-id&amp;gt;:execution:&amp;lt;state-machine-name&amp;gt;:&amp;lt;execution-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;template.yaml&lt;/code&gt;, the &lt;code&gt;AgentCoreGatewayServiceRole&lt;/code&gt; splits these into two statements:&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;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StepFunctionsMCPConnectorLeastPrivilege&lt;/span&gt;
    &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17"&lt;/span&gt;
      &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StateMachineLevelOperations&lt;/span&gt;
          &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;states:StartExecution&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;states:ListExecutions&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;states:DescribeStateMachine&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;SalesIngestionStateMachine.Arn&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExecutionLevelOperations&lt;/span&gt;
          &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;states:DescribeExecution&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;states:GetExecutionHistory&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;states:StopExecution&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:states:${AWS::Region}:${AWS::AccountId}:execution:${SalesIngestionStateMachine.Name}:*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the execution pattern includes the state machine name, the role can only inspect or stop executions of this one pipeline. If someone tricks the agent into stopping a payroll run in the same account, IAM denies it at the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Calling the Gateway over MCP from Python
&lt;/h3&gt;

&lt;p&gt;The client, &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/agent/invoke_agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/invoke_agent.py&lt;/code&gt;&lt;/a&gt;, speaks standard MCP JSON-RPC over SigV4-signed HTTPS. It first calls &lt;code&gt;tools/list&lt;/code&gt; to check the generated schemas, then starts the run with &lt;code&gt;tools/call&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;mcp_start_request&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;jsonrpc&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;2.0&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;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;req-start-1&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;method&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;tools/call&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;params&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;states_StartExecution&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;arguments&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;sales-ingest-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sourceKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source_key&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;After that, the client polls &lt;code&gt;states_DescribeExecution&lt;/code&gt; with backoff until the status is no longer &lt;code&gt;RUNNING&lt;/code&gt;, then pulls &lt;code&gt;states_GetExecutionHistory&lt;/code&gt; for the step-level timing.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Guardrails, and the mistakes I would avoid
&lt;/h2&gt;

&lt;p&gt;I want to be direct about this part. A system prompt that says &lt;em&gt;"never run any workflow except the sales pipeline, and never pass a bad S3 key"&lt;/em&gt; is a wish, not a control. Prompts can be injected, context gets confused, and models change. The protections that actually hold are the ones enforced outside the model: IAM policies, Gateway pinning, and input validation in the state machine.&lt;/p&gt;

&lt;p&gt;Here is how I think about the main risks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operational risk&lt;/th&gt;
&lt;th&gt;What it looks like in practice&lt;/th&gt;
&lt;th&gt;Deterministic mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt injection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A malicious string in a ticket or filename tells the agent to start a cleanup workflow or pass odd arguments to Glue.&lt;/td&gt;
&lt;td&gt;Pin &lt;code&gt;stateMachineArn&lt;/code&gt; in the Gateway target. Enforce the &lt;code&gt;raw/sales/*.parquet&lt;/code&gt; pattern in the &lt;code&gt;ValidateInput&lt;/code&gt; Choice state.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unauthorized execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent tries to find and start a finance or HR workflow in the same account.&lt;/td&gt;
&lt;td&gt;Pin the ARN in the Gateway, and scope &lt;code&gt;states:StartExecution&lt;/code&gt;, &lt;code&gt;ListExecutions&lt;/code&gt;, and &lt;code&gt;DescribeStateMachine&lt;/code&gt; in IAM to that one ARN. Never grant &lt;code&gt;states:*&lt;/code&gt; or &lt;code&gt;Resource: "*"&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stopping the wrong run&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent is asked to cancel a stuck job and picks up an execution ARN that belongs to something important.&lt;/td&gt;
&lt;td&gt;Scope &lt;code&gt;StopExecution&lt;/code&gt;, &lt;code&gt;DescribeExecution&lt;/code&gt;, and &lt;code&gt;GetExecutionHistory&lt;/code&gt; to &lt;code&gt;execution:PinnedStateMachineName:*&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runaway retries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A schema error makes the agent call &lt;code&gt;StartExecution&lt;/code&gt; over and over, burning compute.&lt;/td&gt;
&lt;td&gt;Keep retries inside the state machine with &lt;code&gt;MaxAttempts&lt;/code&gt; and &lt;code&gt;BackoffRate&lt;/code&gt;. Use the &lt;code&gt;name&lt;/code&gt; parameter for idempotency, and cap tool calls per session.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No audit trail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A pipeline changed warehouse tables overnight, and nobody can tell whether a person, a schedule, or an agent started it.&lt;/td&gt;
&lt;td&gt;Use Standard workflows, turn on CloudTrail for Step Functions and Gateway calls, and prefix agent-started execution names (for example &lt;code&gt;agent-run-*&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Long runs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A 45-minute job outlasts the agent's conversation turn.&lt;/td&gt;
&lt;td&gt;Start the run, return the &lt;code&gt;executionArn&lt;/code&gt; straight away, and poll in later turns (or use a callback).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Four mistakes I have seen (and made)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Using the state machine ARN for execution actions.&lt;/strong&gt; The deploy works, &lt;code&gt;StartExecution&lt;/code&gt; works, and then the agent gets an &lt;code&gt;AccessDeniedException&lt;/code&gt; when it checks status. Execution APIs need the &lt;code&gt;:execution:Name:*&lt;/code&gt; form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving &lt;code&gt;stateMachineArn&lt;/code&gt; unpinned during testing.&lt;/strong&gt; The Gateway then makes it a required argument. Now you are hardcoding account IDs and regions into the prompt, and inviting the model to make up ARNs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using an Express workflow for agent monitoring.&lt;/strong&gt; &lt;code&gt;DescribeExecution&lt;/code&gt; and &lt;code&gt;GetExecutionHistory&lt;/code&gt; are designed around Standard workflows. The tools will fail or give you nothing useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swallowing errors.&lt;/strong&gt; If the failure path ends with &lt;code&gt;"End": true&lt;/code&gt;, the execution reports &lt;code&gt;SUCCEEDED&lt;/code&gt;. Your agent will happily tell the user the pipeline worked just as the on-call pager goes off.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  5. Where GPT-6.1 Terra fits
&lt;/h2&gt;

&lt;p&gt;The Gateway is half of an agent. The other half is the model that reads the request, plans the tool calls, and writes the summary. AWS announced in the &lt;a href="https://aws.amazon.com/blogs/aws/aws-weekly-roundup-gpt-6-1-terra-on-bedrock-step-functions-as-agent-tools-and-more-november-2-2026/" rel="noopener noreferrer"&gt;weekly roundup&lt;/a&gt; that &lt;strong&gt;OpenAI GPT-6.1 Terra&lt;/strong&gt; is generally available on Amazon Bedrock. AWS describes it as a fast, cost-efficient model for everyday enterprise work.&lt;/p&gt;

&lt;p&gt;One honest note: any speed, latency, or price figures from AWS or OpenAI are vendor-reported. Measure tool-call accuracy, latency, and cost per run against your own payloads before you put a number in an SLA.&lt;/p&gt;

&lt;p&gt;With that caveat, a fast, cheap model suits this kind of work well. Here are four patterns I find useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short tool loops.&lt;/strong&gt; A typical request means &lt;code&gt;StartExecution&lt;/code&gt;, then &lt;code&gt;DescribeExecution&lt;/code&gt;, then maybe &lt;code&gt;GetExecutionHistory&lt;/code&gt;: three to five calls. A fast model keeps that feeling responsive, which is what makes an ops bot pleasant to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage with context.&lt;/strong&gt; When a run fails, &lt;code&gt;GetExecutionHistory&lt;/code&gt; returns the state transitions and the error. A model can read that, check a knowledge base of runbooks and table contracts, and reply with the cause and the likely fix. A Glue error caused by a column type change is a good example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only assistants.&lt;/strong&gt; Analysts often want to know whether today's partition has loaded before a dashboard goes out. A Gateway target that exposes only &lt;code&gt;ListExecutions&lt;/code&gt;, &lt;code&gt;DescribeExecution&lt;/code&gt;, and &lt;code&gt;GetExecutionHistory&lt;/code&gt; lets a model answer status questions in Slack, with no start or stop permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill coordination.&lt;/strong&gt; Over a multi-day backfill, an agent can walk through verified partitions, start runs with a concurrency limit, check each one, and stop as soon as a data-quality check fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When I would not use this pattern
&lt;/h3&gt;

&lt;p&gt;Not every workflow belongs behind an agent. I would skip it in three cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled or event-driven jobs.&lt;/strong&gt; A nightly run at 02:00 UTC, or a run triggered by an S3 upload, should be an EventBridge rule that starts Step Functions. Adding a model there only adds cost, latency, and another way to fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-volume, low-latency streams.&lt;/strong&gt; Thousands of events per second with tight latency targets belong in Kinesis, Kafka, Lambda, or Express workflows with deterministic routing. Agent tool calls are built for control-plane decisions at human speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple read-only questions.&lt;/strong&gt; "What were total sales in the Northeast yesterday?" is a query, not a workflow. Send it to a text-to-SQL tool on Athena, Redshift, or a semantic layer.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Before you go to production
&lt;/h2&gt;

&lt;p&gt;Before this goes from a sandbox into a real account, I would check these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Standard workflows&lt;/strong&gt; for anything an agent can reach, so the history is durable and queryable.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Pin &lt;code&gt;stateMachineArn&lt;/code&gt;&lt;/strong&gt; in the Gateway target for every operation that takes it.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Two-tier IAM.&lt;/strong&gt; State-machine ARN for state-machine actions, the &lt;code&gt;:execution:Name:*&lt;/code&gt; pattern for execution actions. No wildcard &lt;code&gt;states:*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Timeouts, retries, and catches in the ASL.&lt;/strong&gt; Use &lt;code&gt;TimeoutSeconds&lt;/code&gt;, &lt;code&gt;Retry&lt;/code&gt; with backoff for transient errors, and a &lt;code&gt;Catch&lt;/code&gt; that alerts and ends in a &lt;code&gt;Fail&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Validate inputs at the start&lt;/strong&gt; of the state machine, before any expensive compute.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Log every MCP call&lt;/strong&gt; on the Gateway, with the caller and the payload.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;CloudTrail and CloudWatch Logs&lt;/strong&gt; enabled for Step Functions, for forensics later.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Separate environments.&lt;/strong&gt; Separate Gateway targets and roles for dev, staging, and production, so a staging agent cannot touch production.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Rate limits.&lt;/strong&gt; Guard Glue and warehouse capacity against runaway loops with concurrency checks, idempotent names, or throttling.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Human approval for risky work.&lt;/strong&gt; Truncations, full reloads, and expensive cluster starts should wait on a &lt;code&gt;waitForTaskToken&lt;/code&gt; callback that a person approves in Slack or through a signed link.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The instinct, when building agents, has been to give models direct access to cloud infrastructure. I understand the appeal. But a language model makes a poor distributed transaction coordinator. When an agent touches raw connectors, retries, idempotency, and audit trails turn into text in a prompt, and that is hard to debug at 3 a.m.&lt;/p&gt;

&lt;p&gt;Putting Step Functions Standard workflows behind AgentCore Gateway gives you a cleaner split:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent decides &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;what&lt;/em&gt;.&lt;/strong&gt; It reads the request and fills in the parameters. Step Functions decides &lt;em&gt;how&lt;/em&gt;: retries, error handling, and state history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundaries beat instructions.&lt;/strong&gt; A prompt is a request. IAM scoping, ARN pinning, and validation in the state machine are rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can see what happened.&lt;/strong&gt; Standard workflows keep a full history, so the agent can answer accurately without anyone digging through log streams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI agents do not remove the need for data engineering discipline. If anything, they make it more important. Our job is to design the state machines and the security boundaries that let an agent run a production platform safely, and to be honest with ourselves about where the agent should stop.&lt;/p&gt;




&lt;h3&gt;
  
  
  Resources &amp;amp; Demo Repository
&lt;/h3&gt;

&lt;p&gt;The complete deployable project, including the SAM template, least-privilege IAM roles, the PySpark Glue job, and the MCP client, is in this repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State machine definition:&lt;/strong&gt; &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/state-machine/data-pipeline.asl.json" rel="noopener noreferrer"&gt;&lt;code&gt;state-machine/data-pipeline.asl.json&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AgentCore Gateway config:&lt;/strong&gt; &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/gateway/target.json" rel="noopener noreferrer"&gt;&lt;code&gt;gateway/target.json&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SAM template:&lt;/strong&gt; &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/template.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;template.yaml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python agent client:&lt;/strong&gt; &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/agent/invoke_agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/invoke_agent.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup guide:&lt;/strong&gt; &lt;a href="https://github.com/kien-ly/aws-step-functions-mcp-orchestrator/blob/main/docs/env-setup.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/env-setup.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>data</category>
    </item>
    <item>
      <title>Preparing for Re:Invent 2026</title>
      <dc:creator>Nadtakan Futhoem (Nad)</dc:creator>
      <pubDate>Sun, 08 Nov 2026 00:32:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/preparing-for-reinvent-2026-43ka</link>
      <guid>https://dev.to/aws-builders/preparing-for-reinvent-2026-43ka</guid>
      <description>&lt;p&gt;It's one of those times again! There is one big conference hosted by AWS that I always look forward to. Yep, I'm talking about AWS Re:Invent.&lt;/p&gt;

&lt;p&gt;Every year, I'm excited to see what new services, tools, and announcements AWS has in store.&lt;/p&gt;

&lt;p&gt;Out of curiosity, I did some research and discovered that the conference started back in 2012. That took me back to 2018, when I was just getting started with cloud computing. Fast forward to today, 14 years since the first one, and the conference is still going strong.&lt;/p&gt;

&lt;p&gt;I attended for the first time last year, and it was an incredible experience! &lt;/p&gt;

&lt;p&gt;Being surrounded by thousands of people, all sharing the same excitement, ambition, and curiosity about technology, was something else. I have to admit, I felt a little overwhelmed at first, but in the best possible way.&lt;/p&gt;

&lt;p&gt;There was so much to see, learn, and explore. After experiencing it firsthand, I learned that attending is one thing, but preparing for it beforehand is another.&lt;/p&gt;

&lt;p&gt;So, if you are planning to attend AWS Re:Invent for the first time, or even if you've been before, I'd love to share a few things I learned from my experience to help you prepare and make the most of your time there.&lt;/p&gt;

&lt;p&gt;Along with all the excitement about attending sessions, workshops, and exploring what's new, I have one more thing on my agenda.&lt;/p&gt;

&lt;p&gt;I am excited about something I've been planning for a while. I have one mission: run a 5K.&lt;/p&gt;

&lt;p&gt;It's happening on Wednesday at 6 AM. Yes, you read that right. I'm swapping my usual conference morning routine for a 5K race!&lt;/p&gt;

&lt;p&gt;I'm pretty excited, and it's definitely one of the things I'm looking forward to most.&lt;/p&gt;

&lt;p&gt;I went ahead and created a training plan, of course, using ChatGPT. I set an ambitious goal: run a 5K in 30 minutes. I even wanted to run every day. Sounds ambitious, but I thought it was possible!&lt;/p&gt;

&lt;p&gt;My training plan included easy runs, tempo runs to improve speed, and long runs to build endurance. I ended up running three times a week, adding strength training and taking only one rest day per week.&lt;/p&gt;

&lt;p&gt;A month went by, and I felt great. I'd run during my lunch break or in the evening. I kept tracking my time and pace. Always looking for ways to improve.&lt;/p&gt;

&lt;p&gt;But then I started noticing something wasn't quite right. My body was getting tired, and I was not giving it enough time to recover. I was pushing myself too hard and had not accounted for how much recovery my body actually needed.&lt;/p&gt;

&lt;p&gt;I started reflecting on what I was doing wrong. My body was telling me something, and I needed to listen.&lt;/p&gt;

&lt;p&gt;I stopped running completely for two weeks. Instead, I started doing yoga and gentle stretching. I also stopped lifting weights for a while. I wanted to be gentle to my body. I also spent time in the sauna and hot tub to relax and unwind.&lt;/p&gt;

&lt;p&gt;Looking back, I realized I had overlooked a few things. I was not warming up before my runs or cooling down afterward. I was not stretching either. I had been so focused on running faster, and I forgot about taking care of my body along the way.&lt;/p&gt;

&lt;p&gt;As of today, I feel that taking a step back was the right decision.&lt;/p&gt;

&lt;p&gt;Now, I'm planning my training again, this time with a more balanced approach and the time I have left before race day.&lt;/p&gt;

&lt;p&gt;My new plan includes two running days, one strength training day, two swimming days, and two rest days each week. I want to focus on quality rather than simply doing more.&lt;/p&gt;

&lt;p&gt;There are a few things I am paying more attention to this time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warm up before running. A dynamic warm-up is important to prepare my muscles and joints before hitting the run.&lt;/li&gt;
&lt;li&gt;Paying attention to my running form. Engage my core, maintain a comfortable stride, and be mindful of how my feet land. I also want to improve my balance and lower body strength because running involves repeatedly supporting my body on one leg at a time.&lt;/li&gt;
&lt;li&gt;Make strength training count. Focus on strengthening my lower body and improving stability and balance.&lt;/li&gt;
&lt;li&gt;Add swimming. It's a great way to get more low-impact, full-body exercise while giving my legs a break from running.&lt;/li&gt;
&lt;li&gt;Fuel my body properly. Make sure I'm eating enough, especially getting enough carbohydrates and protein to support my training.&lt;/li&gt;
&lt;li&gt;Get enough sleep. Recovery doesn't happen only at the gym or on the running trail. I had a great run, as always, after getting good sleep last night, so a good night's sleep matters too.&lt;/li&gt;
&lt;li&gt;Stay hydrated. Drink enough water and consider electrolytes(coconut water) when needed, especially after longer or sweatier workouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've made it this far, I hope you've found something useful in my little training journey.&lt;/p&gt;

&lt;p&gt;But most importantly, don't forget to listen to your body. It's easy to get caught up in chasing a goal and forget that recovery or rest is just as important as training.&lt;/p&gt;

&lt;p&gt;And if you are running the 5K at AWS Re:Invent, I'll see you at the starting line. Come say hi!&lt;/p&gt;

&lt;p&gt;See ya!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Kubernetes CPU Requests in Amazon EKS Auto Mode</title>
      <dc:creator>Jesus Aguirre</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:51:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/kubernetes-cpu-requests-in-amazon-eks-auto-mode-421</link>
      <guid>https://dev.to/aws-builders/kubernetes-cpu-requests-in-amazon-eks-auto-mode-421</guid>
      <description>&lt;p&gt;A Pod can remain &lt;code&gt;Pending&lt;/code&gt; even when an Amazon EKS cluster has healthy nodes. One common reason is that no eligible node has enough &lt;em&gt;allocatable&lt;/em&gt; CPU to satisfy the Pod's request.&lt;/p&gt;

&lt;p&gt;In this hands-on experiment, we first deploy a Pod that requests &lt;code&gt;500m&lt;/code&gt; CPU. Kubernetes schedules it on an existing &lt;code&gt;c6a.large&lt;/code&gt; node. We then recreate the Pod with a request of &lt;code&gt;4&lt;/code&gt; CPUs. The existing node cannot accommodate that request, so the Pod becomes unschedulable and EKS Auto Mode provisions additional capacity.&lt;/p&gt;

&lt;p&gt;The experiment demonstrates how a Kubernetes resource request can influence node provisioning. It does &lt;strong&gt;not&lt;/strong&gt; demonstrate scaling based on actual CPU utilization: the container runs &lt;code&gt;sleep 3600&lt;/code&gt;, which does not consume four CPUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we will observe
&lt;/h2&gt;

&lt;p&gt;The experiment follows these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy a Pod requesting &lt;code&gt;500m&lt;/code&gt; CPU and &lt;code&gt;128Mi&lt;/code&gt; memory.&lt;/li&gt;
&lt;li&gt;Confirm that it runs on an existing &lt;code&gt;c6a.large&lt;/code&gt; instance.&lt;/li&gt;
&lt;li&gt;Recreate the Pod with a &lt;code&gt;4&lt;/code&gt; CPU request, leaving memory unchanged.&lt;/li&gt;
&lt;li&gt;Observe the &lt;code&gt;FailedScheduling&lt;/code&gt; event with reason &lt;code&gt;Insufficient cpu&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspect the NodeClaim created by EKS Auto Mode.&lt;/li&gt;
&lt;li&gt;Verify that a new &lt;code&gt;c6a.2xlarge&lt;/code&gt; instance joins the cluster and reaches &lt;code&gt;Ready&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm that the Pod is scheduled on the new node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;c6a.2xlarge&lt;/code&gt; is the instance type observed in this run, not a guaranteed selection. EKS Auto Mode chooses capacity that satisfies the workload and the constraints of the applicable NodePool, subject to available offerings and capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How scheduling leads to provisioning
&lt;/h2&gt;

&lt;p&gt;Kubernetes scheduling and infrastructure provisioning are related, but they are separate operations.&lt;/p&gt;

&lt;p&gt;The Kubernetes Scheduler assigns a Pod to a Node. To make that decision, it checks whether the node has enough remaining &lt;em&gt;allocatable&lt;/em&gt; resources to satisfy the Pod's requests and whether the Pod meets the node's other scheduling constraints.&lt;/p&gt;

&lt;p&gt;A simplified CPU accounting model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU remaining = CPU allocatable - Total CPU requests of scheduled Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a conceptual model, not the complete scheduling algorithm. The Scheduler also considers memory, ephemeral storage, Pod count, node selectors, affinity, taints and tolerations, topology, volumes, and other constraints.&lt;/p&gt;

&lt;p&gt;Three node values are useful when investigating a scheduling decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capacity&lt;/strong&gt; is the total resource capacity reported for the node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocatable&lt;/strong&gt; is the amount Kubernetes makes available to Pods after node-level reservations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocated resources&lt;/strong&gt; in &lt;code&gt;kubectl describe node&lt;/code&gt; summarizes the requests and limits of Pods assigned to that node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An EC2 instance's advertised vCPU count is not the same as the CPU available to new Pods. System and kubelet reservations reduce allocatable capacity, and existing Pod requests consume part of that capacity from the Scheduler's perspective. Actual CPU utilization is not a substitute for request accounting.&lt;/p&gt;

&lt;p&gt;When no eligible node can satisfy a Pod, the Scheduler reports a &lt;code&gt;FailedScheduling&lt;/code&gt; event. &lt;code&gt;Insufficient cpu&lt;/code&gt; means the CPU fit check failed for the eligible nodes at that time. It does not mean that EC2 failed to launch an instance, nor does it describe how busy the CPUs are.&lt;/p&gt;

&lt;p&gt;EKS Auto Mode can respond to an unschedulable Pod by provisioning more compute capacity when an enabled NodePool can satisfy the Pod's requirements. The provisioning system evaluates the workload against NodePool constraints and available infrastructure; Kubernetes does not request a particular EC2 instance type directly.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;NodePool&lt;/strong&gt; defines constraints and disruption settings for nodes that can be provisioned. A &lt;strong&gt;NodeClaim&lt;/strong&gt; is the Kubernetes resource representing a request for a node. After capacity is launched, the instance must register as a Kubernetes Node and become ready before the Scheduler can place the pending Pod on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How EKS Auto Mode Provisions New Compute Capacity&lt;/strong&gt;&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%2F3LK3J6uvHLK7Zm0NKbBaNb0ur8H%2Fimag.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%2F3LK3J6uvHLK7Zm0NKbBaNb0ur8H%2Fimag.webp" title="How EKS Auto Mode Provisions New Compute Capacity" width="798" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment environment
&lt;/h2&gt;

&lt;p&gt;The original experiment used this environment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Recorded configuration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Region&lt;/td&gt;
&lt;td&gt;&lt;code&gt;us-east-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon EKS&lt;/td&gt;
&lt;td&gt;Auto Mode enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes version&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.37&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NodePools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;general-purpose&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload Pod&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cpu-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.ecr.aws/docker/library/busybox:1.36&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container command&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sleep 3600&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 capacity observed&lt;/td&gt;
&lt;td&gt;On-Demand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The handoff records the Kubernetes version as &lt;code&gt;1.37&lt;/code&gt;, but does not include the cluster's exact EKS platform version. It also does not preserve the full live NodePool and NodeClass YAML, Availability Zone, architecture metadata, NodePool limits, or every effective instance constraint. Inspect these values in your own cluster rather than assuming they match the original run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An existing EKS cluster with Auto Mode enabled.&lt;/li&gt;
&lt;li&gt;AWS CLI and &lt;code&gt;kubectl&lt;/code&gt; configured for the cluster.&lt;/li&gt;
&lt;li&gt;Kubernetes permissions to create and delete Pods and inspect Pods, Events, Nodes, NodePools, and NodeClaims.&lt;/li&gt;
&lt;li&gt;AWS IAM permission &lt;code&gt;ec2:DescribeInstances&lt;/code&gt; if you want to query the EC2 instance from the CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The identity used by the operator is separate from the roles used by EKS and the nodes. The cluster IAM role grants permissions to cluster-managed components; the node IAM role is assumed by EC2 nodes. EKS access entries and Kubernetes RBAC determine which IAM principals can access Kubernetes resources. The operator's AWS CLI identity separately needs permission to call EC2 APIs.&lt;/p&gt;

&lt;p&gt;Set the cluster details and connect&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLUSTER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-cluster-name"&lt;/span&gt;

aws eks update-kubeconfig &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

kubectl cluster-info
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Check that the expected Auto Mode NodePools exist&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodepools
kubectl describe nodepool general-purpose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The experiment used the built-in &lt;code&gt;general-purpose&lt;/code&gt; and &lt;code&gt;system&lt;/code&gt; pools. AWS documents the built-in pools as managed configurations; their constraints influence which instance offerings are eligible. The original handoff does not include the complete effective configuration at the time of the experiment, so we cannot attribute the &lt;code&gt;c6a.2xlarge&lt;/code&gt; selection to a specific individual constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Schedule a Pod with a &lt;code&gt;500m&lt;/code&gt; CPU request
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;cpu-test-500m.yaml&lt;/code&gt;&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu-test&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;busybox&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.ecr.aws/docker/library/busybox:1.36&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;128Mi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the manifest and inspect the Pod&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cpu-test-500m.yaml
kubectl get pod cpu-test &lt;span class="nt"&gt;-o&lt;/span&gt; wide
kubectl describe pod cpu-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected outcome:&lt;/strong&gt; the Pod reaches &lt;code&gt;Running&lt;/code&gt; and is assigned to a node with enough allocatable CPU and memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observed in the experiment:&lt;/strong&gt; the Pod ran on an EC2 &lt;code&gt;c6a.large&lt;/code&gt; instance. The original terminal transcript was not preserved, so the instance type is reported as a documented observation rather than a verbatim command output.&lt;/p&gt;

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

&lt;p&gt;Record the assigned Kubernetes node&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod cpu-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.nodeName}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect its resource accounting&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the &lt;code&gt;Capacity&lt;/code&gt;, &lt;code&gt;Allocatable&lt;/code&gt;, and &lt;code&gt;Allocated resources&lt;/code&gt; sections. The first two show the node's reported total and Pod-assignable resources; the last shows the requests and limits of the Pods already assigned to it.&lt;/p&gt;

&lt;p&gt;The original experiment did not preserve the numeric output of &lt;code&gt;kubectl describe node&lt;/code&gt;. We therefore do not infer the exact allocatable CPU on the &lt;code&gt;c6a.large&lt;/code&gt; from its EC2 vCPU specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Increase the CPU request to four CPUs
&lt;/h2&gt;

&lt;p&gt;Now change only the CPU request. Keep the memory request, image, and command the same.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;cpu-test-4cpu.yaml&lt;/code&gt;&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu-test&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;busybox&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.ecr.aws/docker/library/busybox:1.36&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;128Mi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Delete the first Pod and create the new one&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod cpu-test
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cpu-test-4cpu.yaml

kubectl get pod cpu-test &lt;span class="nt"&gt;-o&lt;/span&gt; wide
kubectl describe pod cpu-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Observed in the experiment:&lt;/strong&gt; the Pod remained &lt;code&gt;Pending&lt;/code&gt;. Its events recorded &lt;code&gt;FailedScheduling&lt;/code&gt; with reason &lt;code&gt;Insufficient cpu&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The original complete event message was not retained; this is a summary of the recorded observation, not a verbatim terminal transcript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected outcome:&lt;/strong&gt; while no eligible node can satisfy the Pod's request and other scheduling constraints, the Pod remains pending. Exact event wording and counts can vary.&lt;/p&gt;

&lt;p&gt;The request changed from &lt;code&gt;500m&lt;/code&gt; (half a CPU) to &lt;code&gt;4&lt;/code&gt; CPUs, while memory remained at &lt;code&gt;128Mi&lt;/code&gt;. This makes CPU request the workload variable changed in the experiment.&lt;/p&gt;

&lt;p&gt;The key point is that a request is used for scheduling even when the container is not consuming that amount of CPU at runtime. The &lt;code&gt;sleep 3600&lt;/code&gt; process does not generate four CPUs of load.&lt;/p&gt;

&lt;p&gt;The experiment is about requested resources and scheduling feasibility, not utilization-based scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Inspect the NodeClaim created by EKS Auto Mode
&lt;/h2&gt;

&lt;p&gt;When a Pod is unschedulable, EKS Auto Mode can provision additional capacity if an applicable NodePool can meet the Pod's requirements and provisioning is not blocked by constraints or infrastructure availability.&lt;/p&gt;

&lt;p&gt;List the NodeClaims&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodeclaims &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the NodeClaim created during the experiment&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe nodeclaim &amp;lt;nodeclaim-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Observed in the experiment:&lt;/strong&gt; EKS Auto Mode created a NodeClaim in the &lt;code&gt;general-purpose&lt;/code&gt; pool. Its generated name was &lt;code&gt;general-purpose-rf7fr&lt;/code&gt;. This name is lab evidence only; NodeClaim names are generated and should not be hard-coded.&lt;/p&gt;

&lt;p&gt;A NodeClaim provides the Kubernetes-side record of the capacity request and its provisioning lifecycle. Inspect its conditions and, when available, its &lt;code&gt;status.providerID&lt;/code&gt;. A NodeClaim being present does not by itself mean that the EC2 instance has registered as a Ready Kubernetes Node.&lt;/p&gt;

&lt;p&gt;The experiment also recorded that the original node entered termination with reason &lt;code&gt;Empty&lt;/code&gt;. Preserve that as a separate lifecycle observation: the available evidence does not establish that the change to the CPU request directly caused the old node's termination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Verify the new node and the Pod placement
&lt;/h2&gt;

&lt;p&gt;List the nodes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Observed in the experiment:&lt;/strong&gt; EKS Auto Mode provisioned a new &lt;code&gt;c6a.2xlarge&lt;/code&gt; EC2 instance.&lt;/p&gt;

&lt;p&gt;The instance was On-Demand and the corresponding Kubernetes Node reached &lt;code&gt;Ready&lt;/code&gt;. The Pod was then scheduled successfully.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;c6a.2xlarge&lt;/code&gt; has eight advertised vCPUs, but those eight vCPUs should not be treated as eight CPUs of Pod-allocatable capacity.&lt;/p&gt;

&lt;p&gt;Check the registered node's Kubernetes resource values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod cpu-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.nodeName}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl describe node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
kubectl get pod cpu-test &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected outcome:&lt;/strong&gt; the Pod reaches &lt;code&gt;Running&lt;/code&gt; and its assigned node is the newly provisioned node. The exact instance type is not guaranteed; another compatible offering may be selected in a different run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlate the Pod, Node, NodeClaim, and EC2 instance
&lt;/h3&gt;

&lt;p&gt;The Kubernetes Node's &lt;code&gt;spec.providerID&lt;/code&gt; is the link to the cloud instance. For an AWS-backed node, the final component of the provider ID is the EC2 instance ID.&lt;/p&gt;

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

&lt;p&gt;First, get the node assigned to the Pod and its provider ID&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod cpu-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.nodeName}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;PROVIDER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.providerID}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;INSTANCE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROVIDER_ID&lt;/span&gt;&lt;span class="p"&gt;##*/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Node: %s\nProvider ID: %s\nInstance ID: %s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROVIDER_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the NodeClaim with the matching provider ID. This command uses &lt;code&gt;jq&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;&lt;span class="nv"&gt;NODECLAIM_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get nodeclaims &lt;span class="nt"&gt;-o&lt;/span&gt; json | &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; provider_id &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROVIDER_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'.items[] | select(.status.providerID == $provider_id) | .metadata.name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'NodeClaim: %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODECLAIM_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
kubectl describe nodeclaim &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODECLAIM_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the result is empty, the NodeClaim's provider ID may not yet be populated. Inspect &lt;code&gt;kubectl get nodeclaims -o wide&lt;/code&gt; and retry after the resource progresses.&lt;/p&gt;

&lt;p&gt;Finally, query EC2&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Reservations[].Instances[].{
    InstanceId:InstanceId,
    InstanceType:InstanceType,
    State:State.Name,
    Architecture:Architecture,
    AvailabilityZone:Placement.AvailabilityZone
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command requires the operator's AWS identity to have &lt;code&gt;ec2:DescribeInstances&lt;/code&gt;. The Kubernetes commands require read access to Pods, Nodes, and NodeClaims. These are separate permission systems.&lt;/p&gt;

&lt;p&gt;The experiment recorded the new instance as &lt;code&gt;c6a.2xlarge&lt;/code&gt; with On-Demand capacity. The instance ID and complete original AWS CLI output were not retained in the article handoff, so the reusable commands discover the ID dynamically rather than embedding a lab-specific value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mapping Kubernetes Resources to EC2 Instances&lt;/strong&gt;&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%2F3LK3itpsA8YwciSkCZDH7nBiqCy%2Fimag.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%2F3LK3itpsA8YwciSkCZDH7nBiqCy%2Fimag.webp" title="Mapping Kubernetes Resources to EC2 Instances" width="799" height="175"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a custom NodePool for a workload class
&lt;/h2&gt;

&lt;p&gt;The experiment used the built-in &lt;code&gt;general-purpose&lt;/code&gt; NodePool. If you need to express workload-specific constraints, you can create a custom NodePool and give its nodes a label.&lt;/p&gt;

&lt;p&gt;For example, this NodePool labels its nodes with &lt;code&gt;workload-class: cpu-oriented&lt;/code&gt;&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu-oriented&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;workload-class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu-oriented&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nodeClassRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeClass&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="na"&gt;requirements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-category&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;c&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-cpu&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;32"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/arch&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/capacity-type&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;on-demand&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an illustrative custom configuration, not the NodePool used in the original experiment. It assumes the cluster has the Auto Mode &lt;code&gt;default&lt;/code&gt; NodeClass. Check the target cluster's available NodeClass and supported requirements before applying it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How NodePool Constraints Shape Eligible Compute Capacity&lt;/strong&gt;&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%2F3LK3rB0wwGtMofUmXsdwyFHPPV6%2Fimag.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%2F3LK3rB0wwGtMofUmXsdwyFHPPV6%2Fimag.webp" title="How NodePool Constraints Shape Eligible Compute Capacity" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Pod can target nodes from this pool using a &lt;code&gt;nodeSelector&lt;/code&gt;&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;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nodeSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workload-class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu-oriented&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selector value must match the label defined under &lt;code&gt;spec.template.metadata.labels&lt;/code&gt; in the NodePool. If the values do not match, the Pod will not be eligible for nodes from this pool.&lt;/p&gt;

&lt;p&gt;The NodePool constrains the set of eligible capacity; it does not guarantee that a particular EC2 size will be selected. Even with these requirements, the chosen instance depends on the Pod's resource requests, all applicable constraints, and available capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the experiment teaches us
&lt;/h2&gt;

&lt;p&gt;The two configurations produced the following documented results&lt;/p&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;Initial Pod&lt;/th&gt;
&lt;th&gt;Updated Pod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;500m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;128Mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;128Mi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observed EC2 instance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c6a.large&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c6a.2xlarge&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advertised vCPUs&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduling result&lt;/td&gt;
&lt;td&gt;Scheduled on existing node&lt;/td&gt;
&lt;td&gt;Initially &lt;code&gt;FailedScheduling&lt;/code&gt;; scheduled after new node became Ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important change was not an increase in measured CPU consumption. It was an increase in the CPU request that the Scheduler had to accommodate.&lt;/p&gt;

&lt;p&gt;The sequence demonstrates the interaction between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pod requests&lt;/strong&gt;, which describe the resources Kubernetes must account for when scheduling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Scheduler&lt;/strong&gt;, which determines whether an eligible node can accept the Pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodePools&lt;/strong&gt;, which constrain the kinds of nodes the provisioning system can create.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodeClaims&lt;/strong&gt;, which represent requested node capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2&lt;/strong&gt;, which supplies the compute instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node registration and readiness&lt;/strong&gt;, which make the new capacity available to Kubernetes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old node's &lt;code&gt;Empty&lt;/code&gt; termination reason and the transient &lt;code&gt;InvalidDiskCapacity&lt;/code&gt; warnings observed during node startup are additional lab observations. Both nodes subsequently became &lt;code&gt;Ready&lt;/code&gt;; the available evidence does not establish a more specific cause for either observation, so neither should be treated as proof of a persistent issue or as a direct consequence of the CPU request change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical considerations
&lt;/h2&gt;

&lt;p&gt;CPU requests affect scheduling and can therefore affect the infrastructure required to run a workload. A request that is much higher than the workload needs can lead to inefficient placement or larger infrastructure. A request that is too low can increase packing density but may contribute to contention when workloads need more CPU concurrently.&lt;/p&gt;

&lt;p&gt;Use application measurements and realistic workload testing to set requests. For production, also consider replicas, disruption budgets, topology spread, graceful termination, and the cost implications of the eligible instance types.&lt;/p&gt;

&lt;p&gt;The experiment used a single Pod to isolate scheduling behavior. It does not demonstrate high availability, performance under load, or the minimum instance size required for a four-CPU request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up
&lt;/h2&gt;

&lt;p&gt;Delete the workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod cpu-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you created a custom NodePool for this exercise, remove it when it is no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete nodepool cpu-oriented
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only delete the EKS cluster if it was created exclusively for this experiment and you have confirmed that deletion is intended. Review related AWS resources and billing independently; deleting a cluster should not be assumed to remove every shared networking or IAM resource.&lt;/p&gt;

&lt;p&gt;The original lab cleanup removed the Pod and EKS cluster. The observed EC2 instances reached &lt;code&gt;terminated&lt;/code&gt;; shared networking resources and IAM roles and policies were preserved. The handoff does not establish that every auxiliary resource, such as ENIs or security groups, was removed.&lt;/p&gt;

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

&lt;p&gt;A Kubernetes CPU request is more than a description of an application's desired resources: it is an input to scheduling. When a Pod's request changes from &lt;code&gt;500m&lt;/code&gt; to &lt;code&gt;4&lt;/code&gt; CPUs, a node that previously accommodated it may no longer be eligible.&lt;/p&gt;

&lt;p&gt;In this experiment, the &lt;code&gt;500m&lt;/code&gt; Pod ran on an existing &lt;code&gt;c6a.large&lt;/code&gt;. After the request increased to &lt;code&gt;4&lt;/code&gt;, the Pod became pending with &lt;code&gt;FailedScheduling&lt;/code&gt; and &lt;code&gt;Insufficient cpu&lt;/code&gt;. EKS Auto Mode created a NodeClaim, provisioned a new &lt;code&gt;c6a.2xlarge&lt;/code&gt; instance, and made the node Ready. The Pod was then scheduled successfully.&lt;/p&gt;

&lt;p&gt;The instance type is specific to this run, not a fixed outcome. The reusable lesson is to trace the entire path: inspect the Pod's requests and scheduling events, review the matching NodePool, follow the NodeClaim, correlate its provider ID with the Kubernetes Node and EC2 instance, and confirm that the node is Ready before concluding that the workload has usable capacity.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>kubernetes</category>
      <category>aws</category>
      <category>architecture</category>
    </item>
    <item>
      <title>NexOps: Build CloudOps Agent Using Strands+Bedrock+Terraform</title>
      <dc:creator>Anirban Das</dc:creator>
      <pubDate>Sat, 07 Nov 2026 12:26:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/nexops-an-ai-assistant-for-aws-cloud-operations-37i4</link>
      <guid>https://dev.to/aws-builders/nexops-an-ai-assistant-for-aws-cloud-operations-37i4</guid>
      <description>&lt;h2&gt;
  
  
  Overview of NexOps
&lt;/h2&gt;

&lt;p&gt;NexOps is a cloud operations assistant that lets users ask questions about AWS infrastructure in plain language. It combines a lightweight web interface with an AI agent that can inspect selected AWS services, gather operational evidence, and explain findings in a clear and practical way.&lt;/p&gt;

&lt;p&gt;The backend uses the Strands Agents framework and Amazon Bedrock. For each chat request, it creates a fresh agent instance and gives it a set of service-specific tools implemented with Boto3. The application is designed to support operations teams with investigation and routine administration while keeping the scope of available actions explicit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask questions about infrastructure, health, security posture, and costs.&lt;/li&gt;
&lt;li&gt;Gather details from supported AWS services through dedicated tools.&lt;/li&gt;
&lt;li&gt;Receive summaries that distinguish observed facts from possible causes.&lt;/li&gt;
&lt;li&gt;Use gated EC2 lifecycle and Systems Manager operations when the deployment is configured to allow them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NexOps is not an unrestricted AWS command runner. Tool coverage depends on the AWS account, region, IAM permissions, and service prerequisites available to the running application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business Problems
&lt;/h2&gt;

&lt;p&gt;Cloud teams often need to inspect several consoles and services to answer a basic operational question. During an incident, switching between instance state, monitoring data, logs, alarms, and recent changes can slow down diagnosis and make it harder to share a concise summary with the rest of the team.&lt;/p&gt;

&lt;p&gt;NexOps brings selected checks together behind a conversational interface. It can help reduce repetitive lookup work and make operational information more accessible, while leaving decisions and approvals with the people responsible for the environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scattered operational data:&lt;/strong&gt; Relevant details may live in EC2, CloudWatch, CloudTrail, AWS Config, IAM, and other service views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow first-pass investigation:&lt;/strong&gt; Engineers may repeat the same inventory and health checks before they can focus on likely causes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-to-summarize findings:&lt;/strong&gt; Raw service responses can be lengthy and difficult to turn into a useful incident update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security review overhead:&lt;/strong&gt; Teams need a convenient way to inspect selected IAM, encryption, access, and network controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost visibility:&lt;/strong&gt; Service costs and low-utilization candidates can be difficult to spot during routine operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application supports these workflows by collecting targeted evidence and presenting it in natural language. Findings such as low CPU, aged access keys, or broad policy statements are review signals; they are not automatic judgments or remediation decisions.&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%2Fd1lvw7oews1ey18si1sm.jpg" 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%2Fd1lvw7oews1ey18si1sm.jpg" alt=" " width="799" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NexOps separates the user interface, request routing, and AWS operations into deployable components. The browser loads a static frontend served by NGINX on ECS Fargate. Chat requests pass through Amazon API Gateway and a private integration to the backend Application Load Balancer, which forwards them to the FastAPI service running in ECS Fargate.&lt;/p&gt;

&lt;p&gt;For each chat request, FastAPI validates the input and optional region, constructs a Strands agent backed by Amazon Bedrock, and returns the agent's response. The agent selects from registered Python tools; those tools use Boto3 to call supported AWS APIs. The AWS identity available to the task, typically through its ECS task role, determines which calls succeed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Static HTML, CSS, and JavaScript served by NGINX in a Fargate task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request entry:&lt;/strong&gt; Public frontend Application Load Balancer; API Gateway HTTP API for chat traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private backend path:&lt;/strong&gt; API Gateway VPC Link to an internal Application Load Balancer and backend ECS service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent service:&lt;/strong&gt; FastAPI endpoints, Strands orchestration, and service-specific Boto3 tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Amazon Bedrock, using the configured model ID (Nova Micro by default in the application).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; Terraform definitions for ECR, ECS, load balancers, API Gateway, logging, and supporting resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend creates a new agent for every request, so chat history is not persisted across requests. The frontend and API deployment settings, including API URL, CORS origins, authentication, and network access, must be aligned for a production deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Application Does
&lt;/h2&gt;

&lt;p&gt;NexOps translates an operator's question into one or more targeted tool calls and then summarizes the returned evidence. The registered tools cover selected parts of AWS rather than every service or every resource in an account.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inventory:&lt;/strong&gt; Inspect supported EC2, EBS, VPC, security group, S3, Lambda, RDS, and ECS resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring and troubleshooting:&lt;/strong&gt; Query CloudWatch metrics, alarms, log groups, and logs; inspect EC2 status and console output; and gather recent CloudTrail changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and compliance review:&lt;/strong&gt; Inspect selected IAM users, groups, roles, policies, MFA and access-key metadata, security group rules, encryption settings, and AWS Config results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost analysis:&lt;/strong&gt; Review Cost Explorer service costs, supported resource-level EC2 costs, and low-CPU candidates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approved operations:&lt;/strong&gt; Start, stop, or reboot EC2 instances and invoke restricted Systems Manager actions when the relevant application gates, allowlists, IAM permissions, and AWS prerequisites are satisfied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result verification:&lt;/strong&gt; Where supported, inspect the resulting state after an operation and report whether it completed, failed, or remains in progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent is instructed to use evidence for live AWS claims, report the region and scope it checked, and identify uncertainty. It does not provide a generic AWS API executor, IAM write tools, resource deletion, or arbitrary shell access. A tool being available also does not mean that the task role has permission to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functional Workflow
&lt;/h2&gt;

&lt;p&gt;The workflow begins when a user enters a question in the web application. The frontend sends the message and, optionally, a region to the API Gateway endpoint. API Gateway routes the request over its VPC Link to the backend service.&lt;/p&gt;

&lt;p&gt;FastAPI validates the request and applies the configured API-token behavior. It then creates a Strands agent using the configured Bedrock model and gives the agent the supported tool set and operational system prompt. The agent chooses relevant read-only checks first for investigative requests, calls AWS through Boto3, and uses the returned results to produce a concise response.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user submits a natural-language question and optional AWS region.&lt;/li&gt;
&lt;li&gt;The frontend posts the request to the API Gateway chat route.&lt;/li&gt;
&lt;li&gt;API Gateway forwards it through the VPC Link and internal load balancer to FastAPI.&lt;/li&gt;
&lt;li&gt;The backend validates the input and creates a request-scoped agent.&lt;/li&gt;
&lt;li&gt;The agent selects the narrowest relevant AWS tools and gathers results.&lt;/li&gt;
&lt;li&gt;For a clearly authorized state change, the agent may call a gated operation if deployment policy permits it.&lt;/li&gt;
&lt;li&gt;The agent summarizes evidence, limitations, errors, and action status for the frontend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The health endpoint provides a liveness response and does not contact AWS or Bedrock. Chat calls do not share persisted conversation history. An accepted AWS operation is reported as submitted until a verification check confirms its resulting state.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Components
&lt;/h2&gt;

&lt;p&gt;The infrastructure uses managed AWS services to host the frontend and backend, expose their HTTP entry points, and connect the agent to AWS operations and model inference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon ECS on AWS Fargate:&lt;/strong&gt; Runs the NGINX frontend container and FastAPI backend container without managing EC2 hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon ECR:&lt;/strong&gt; Stores the frontend and backend container images; the Terraform configuration enables image scanning on push.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elastic Load Balancing (Application Load Balancer):&lt;/strong&gt; A public-facing frontend ALB serves the web tier, while an internal ALB receives backend traffic from API Gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon API Gateway HTTP API:&lt;/strong&gt; Exposes the chat route and connects privately to the backend through a VPC Link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock:&lt;/strong&gt; Provides model inference for the Strands agent. The model ID and Bedrock region are runtime configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS IAM:&lt;/strong&gt; The ECS task role supplies the application's AWS permissions; the execution role supports ECS task startup integrations such as image pulls and log delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon CloudWatch Logs:&lt;/strong&gt; Receives container logs and supports the application's monitoring and log-search tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS KMS:&lt;/strong&gt; Supplies keys referenced by ECS execute-command logging configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon VPC:&lt;/strong&gt; Places ECS tasks and the internal backend path in private subnets, with security groups controlling connectivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon S3:&lt;/strong&gt; Stores Terraform state through the configured remote backend; the state bucket must already exist and be accessible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Systems Manager, AWS Config, AWS CloudTrail, and Cost Explorer:&lt;/strong&gt; Supply operational, compliance, change-history, and cost data when enabled and permitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security Groups
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Public Frontend ALB Security Group:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Public-CloudOps-Webapp-LB&lt;/li&gt;
&lt;li&gt;Inbound: HTTPS from Anywhere&lt;/li&gt;
&lt;li&gt;Outbound: Default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend ECS Security Group:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: CloudOps-Web-App&lt;/li&gt;
&lt;li&gt;Inbound: TCP at 8080 port from Public-CloudOps-Webapp-LB&lt;/li&gt;
&lt;li&gt;Outbound: Default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;VPC Link Security Group:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: cloudops-vpc-link-sg&lt;/li&gt;
&lt;li&gt;Inbound: HTTPS from CloudOps-Web-App&lt;/li&gt;
&lt;li&gt;Outbound: HTTPS to ALBSG (Internal ALB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Internal ALB Security Group:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: InternalALBSG&lt;/li&gt;
&lt;li&gt;Inbound: HTTPS from cloudops-vpc-link-sg&lt;/li&gt;
&lt;li&gt;Outbound: Default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backend ECS Security Group:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: StrandsAgentSG&lt;/li&gt;
&lt;li&gt;Inbound: TCP at 8000 port from InternalALBSG&lt;/li&gt;
&lt;li&gt;Outbound: Default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some resources and IAM roles are looked up by Terraform rather than created by these modules. Operators must review the configured role policies, network rules, model access, and service prerequisites for their own AWS environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Steps
&lt;/h2&gt;

&lt;p&gt;NexOps is implemented as two application components: a Python backend and a static web frontend. The backend builds a request-scoped Strands agent and registers the AWS tools it can use. The frontend is served by NGINX and proxies chat requests to the API Gateway endpoint configured for its container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Complete Repository Folder Structure
&lt;/h3&gt;

&lt;p&gt;The tree below shows the files tracked in the repository and how the frontend, backend, AWS tools, Terraform infrastructure, and CI pipelines are organized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-cloudops-agent/
├── .gitlab-ci.yml
├── .gitlab-ci-backend.yml
├── .gitlab-ci-frontend.yml
├── Backend/
│   ├── .dockerignore
│   ├── .env.example
│   ├── .gitignore
│   ├── Dockerfile.backend
│   ├── README.md
│   ├── app.py
│   ├── agent/
│   │   ├── orchestrator.py
│   │   └── system_prompt.py
│   ├── services/
│   │   ├── __init__.py
│   │   ├── aws_clients.py
│   │   ├── policy.py
│   │   └── result_formatter.py
│   ├── tools/
│   │   ├── __init__.py
│   │   ├── cloudtrail_tools.py
│   │   ├── cloudwatch_alarms.py
│   │   ├── cloudwatch_logs.py
│   │   ├── cloudwatch_metrics.py
│   │   ├── cloudwatch_tools.py
│   │   ├── config_tools.py
│   │   ├── cost_optimization.py
│   │   ├── ebs_tools.py
│   │   ├── ec2_inventory.py
│   │   ├── ec2_lifecycle.py
│   │   ├── ec2_tools.py
│   │   ├── ecs_tools.py
│   │   ├── iam_tools.py
│   │   ├── lambda_tools.py
│   │   ├── network_tools.py
│   │   ├── policy_checks.py
│   │   ├── rds_tools.py
│   │   ├── remediation.py
│   │   ├── s3_tools.py
│   │   └── ssm_operations.py
│   ├── Backend_Infra/
│   │   ├── alb.tf
│   │   ├── apigateway.tf
│   │   ├── data.tf
│   │   ├── ecr.tf
│   │   ├── ecs.tf
│   │   ├── output.tf
│   │   ├── provider.tf
│   │   ├── terraform.tfvars
│   │   └── variable.tf
│   └── requirements.txt
├── Frontend/
│   ├── .dockerignore
│   ├── .gitignore
│   ├── Dockerfile.frontend
│   ├── README.md
│   ├── cloudopsweb/
│   │   ├── README.md
│   │   ├── app.js
│   │   ├── index.html
│   │   ├── nginx.conf
│   │   └── styles.css
│   └── Frontend_Infra/
│       ├── alb.tf
│       ├── data.tf
│       ├── ecr.tf
│       ├── ecs.tf
│       ├── output.tf
│       ├── provider.tf
│       ├── terraform.tfvars
│       └── variable.tf
├── PROJECT_BLOG.md
└── __pycache__/
    └── app.cpython-313.pyc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1. Configure the AWS environment:&lt;/strong&gt; Choose the target account and region, confirm Bedrock model access, and prepare the VPC, subnets, security groups, Terraform state bucket, and required IAM roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2. Configure the backend:&lt;/strong&gt; Set the Bedrock model and region, default AWS region, and any account or region restrictions. Grant the ECS task role only the AWS API permissions required by the tools to be enabled. &lt;code&gt;Backend/agent/orchestrator.py&lt;/code&gt; constructs the Bedrock model and passes it, the registered tools, and system prompt to Strands:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&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="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEDROCK_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;us.amazon.nova-micro-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;bedrock_region&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="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEDROCK_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_REGION&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;us-east-1&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="nf"&gt;strip&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="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;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bedrock_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEDROCK_MAX_TOKENS&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;2048&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;return&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&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="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&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;cloudops-agent&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3. Package the backend:&lt;/strong&gt; &lt;code&gt;Backend/Dockerfile.backend&lt;/code&gt; installs Python dependencies and runs the FastAPI service with Uvicorn on port 8000:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt ./requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4. Package the frontend:&lt;/strong&gt; &lt;code&gt;Frontend/Dockerfile.frontend&lt;/code&gt; uses NGINX to serve the browser UI. NGINX proxies &lt;code&gt;/api/chat&lt;/code&gt; to the API Gateway endpoint provided to the frontend task as &lt;code&gt;API_GATEWAY_URL&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; cloudopsweb/nginx.conf /etc/nginx/templates/default.conf.template&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; cloudopsweb/index.html /usr/share/nginx/html/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; cloudopsweb/styles.css /usr/share/nginx/html/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; cloudopsweb/app.js /usr/share/nginx/html/&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["nginx", "-g", "daemon off;"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5. Configure operational safeguards:&lt;/strong&gt; Keep mutations disabled unless they are deliberately required. When enabled, restrict them to approved account IDs and regions; separately allowlist reviewed SSM documents. Configure API authentication and production mode before exposing chat to untrusted clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6. Build and publish images:&lt;/strong&gt; Build the backend and frontend containers from their respective Dockerfiles, then push both images to their ECR repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7. Deploy infrastructure:&lt;/strong&gt; Review and apply Terraform in &lt;code&gt;Backend/Backend_Infra&lt;/code&gt; and &lt;code&gt;Frontend/Frontend_Infra&lt;/code&gt;. Confirm the looked-up roles, network resources, image names, frontend API endpoint, CORS origins, and load balancer health checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8. Verify the application path:&lt;/strong&gt; Check backend &lt;code&gt;/health&lt;/code&gt;, confirm ECS tasks become healthy, load the frontend through its ALB, and submit a chat request through API Gateway in a non-production environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9. Validate tool access and operations:&lt;/strong&gt; Test read-only inventory and monitoring calls first. If lifecycle or SSM actions are needed, validate the gates, exact allowlists, IAM permissions, target state, and post-action verification behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10. Operate and improve:&lt;/strong&gt; Monitor container logs and service health, review IAM access periodically, and update model, infrastructure, and tool configuration as requirements change.&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%2Fsab0pkmx7vit0fjcgiw9.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%2Fsab0pkmx7vit0fjcgiw9.png" alt=" " width="800" height="61"&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%2Fiqn0nkwusjnwmjp34brn.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%2Fiqn0nkwusjnwmjp34brn.png" alt=" " width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Registered Agent Tools
&lt;/h3&gt;

&lt;p&gt;These are the functions registered in &lt;code&gt;Backend/agent/orchestrator.py&lt;/code&gt;. The agent selects tools based on each request; it does not call every tool for every chat. Access also depends on IAM permissions and AWS service configuration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2 and resource inventory:&lt;/strong&gt; &lt;code&gt;list_ec2_instances&lt;/code&gt;, &lt;code&gt;get_ec2_instance_details&lt;/code&gt;, &lt;code&gt;get_ec2_instance_state&lt;/code&gt;, &lt;code&gt;get_ec2_instance_status&lt;/code&gt;, &lt;code&gt;get_ec2_console_output&lt;/code&gt;, &lt;code&gt;list_aws_resources&lt;/code&gt;, &lt;code&gt;get_resource_tags&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch:&lt;/strong&gt; &lt;code&gt;get_cloudwatch_metrics&lt;/code&gt;, &lt;code&gt;list_cloudwatch_alarms&lt;/code&gt;, &lt;code&gt;get_cloudwatch_alarm_history&lt;/code&gt;, &lt;code&gt;search_cloudwatch_logs&lt;/code&gt;, &lt;code&gt;list_cloudwatch_log_groups&lt;/code&gt;, &lt;code&gt;get_cloudwatch_dashboard&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail and AWS Config:&lt;/strong&gt; &lt;code&gt;get_recent_account_events&lt;/code&gt;, &lt;code&gt;get_resource_event_history&lt;/code&gt;, &lt;code&gt;list_config_rules&lt;/code&gt;, &lt;code&gt;get_config_resource_history&lt;/code&gt;, &lt;code&gt;get_config_compliance&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM inspection:&lt;/strong&gt; &lt;code&gt;get_iam_role_details&lt;/code&gt;, &lt;code&gt;list_iam_role_policies&lt;/code&gt;, &lt;code&gt;simulate_iam_policy&lt;/code&gt;, &lt;code&gt;list_iam_users&lt;/code&gt;, &lt;code&gt;list_iam_groups&lt;/code&gt;, &lt;code&gt;get_iam_user_details&lt;/code&gt;, &lt;code&gt;get_iam_group_details&lt;/code&gt;, &lt;code&gt;get_group_members&lt;/code&gt;, &lt;code&gt;list_user_groups&lt;/code&gt;, &lt;code&gt;get_user_attached_policies&lt;/code&gt;, &lt;code&gt;get_group_attached_policies&lt;/code&gt;, &lt;code&gt;get_user_inline_policies&lt;/code&gt;, &lt;code&gt;get_group_inline_policies&lt;/code&gt;, &lt;code&gt;list_user_access_keys&lt;/code&gt;, &lt;code&gt;list_user_mfa_devices&lt;/code&gt;, &lt;code&gt;analyze_group_permissions&lt;/code&gt;, &lt;code&gt;identify_empty_iam_groups&lt;/code&gt;, &lt;code&gt;check_users_without_mfa&lt;/code&gt;, &lt;code&gt;identify_groups_with_admin_access&lt;/code&gt;, &lt;code&gt;check_user_access_key_age&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC and networking:&lt;/strong&gt; &lt;code&gt;list_vpcs&lt;/code&gt;, &lt;code&gt;list_subnets&lt;/code&gt;, &lt;code&gt;list_security_groups&lt;/code&gt;, &lt;code&gt;list_route_tables&lt;/code&gt;, &lt;code&gt;list_network_acls&lt;/code&gt;, &lt;code&gt;check_security_group_rules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EBS:&lt;/strong&gt; &lt;code&gt;list_ebs_volumes&lt;/code&gt;, &lt;code&gt;get_ebs_volume_details&lt;/code&gt;, &lt;code&gt;check_ebs_encryption&lt;/code&gt;, &lt;code&gt;list_ebs_snapshots&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS:&lt;/strong&gt; &lt;code&gt;list_rds_instances&lt;/code&gt;, &lt;code&gt;get_rds_instance_details&lt;/code&gt;, &lt;code&gt;check_rds_encryption&lt;/code&gt;, &lt;code&gt;get_rds_events&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3:&lt;/strong&gt; &lt;code&gt;list_s3_buckets&lt;/code&gt;, &lt;code&gt;get_s3_bucket_details&lt;/code&gt;, &lt;code&gt;check_s3_encryption&lt;/code&gt;, &lt;code&gt;check_s3_public_access&lt;/code&gt;, &lt;code&gt;list_s3_lifecycle_rules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda:&lt;/strong&gt; &lt;code&gt;list_lambda_functions&lt;/code&gt;, &lt;code&gt;get_lambda_function_details&lt;/code&gt;, &lt;code&gt;get_lambda_function_errors&lt;/code&gt;, &lt;code&gt;list_lambda_event_sources&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS:&lt;/strong&gt; &lt;code&gt;list_ecs_clusters&lt;/code&gt;, &lt;code&gt;list_ecs_services&lt;/code&gt;, &lt;code&gt;list_ecs_tasks&lt;/code&gt;, &lt;code&gt;get_ecs_service_events&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnosis and change verification:&lt;/strong&gt; &lt;code&gt;diagnose_ec2_health&lt;/code&gt;, &lt;code&gt;analyze_cloudwatch_metrics&lt;/code&gt;, &lt;code&gt;analyze_instance_logs&lt;/code&gt;, &lt;code&gt;get_recent_resource_changes&lt;/code&gt;, &lt;code&gt;verify_remediation_result&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Systems Manager:&lt;/strong&gt; &lt;code&gt;check_ssm_managed_status&lt;/code&gt;, &lt;code&gt;check_patch_compliance&lt;/code&gt;, &lt;code&gt;get_ssm_command_invocation&lt;/code&gt;, &lt;code&gt;get_ssm_automation_execution&lt;/code&gt;, &lt;code&gt;run_ssm_command&lt;/code&gt;, &lt;code&gt;run_ssm_automation&lt;/code&gt;, &lt;code&gt;restart_application_service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2 lifecycle:&lt;/strong&gt; &lt;code&gt;start_ec2_instance&lt;/code&gt;, &lt;code&gt;stop_ec2_instance&lt;/code&gt;, &lt;code&gt;reboot_ec2_instance&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security policy checks:&lt;/strong&gt; &lt;code&gt;check_iam_resource_policy&lt;/code&gt;, &lt;code&gt;check_resource_encryption&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost analysis:&lt;/strong&gt; &lt;code&gt;get_service_costs&lt;/code&gt;, &lt;code&gt;get_resource_costs&lt;/code&gt;, &lt;code&gt;find_idle_resources&lt;/code&gt;, &lt;code&gt;estimate_ec2_cost_impact&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository documents several deployment settings that need attention before production exposure. In particular, the backend Terraform task definition currently enables the mutation gate for a specific account and region, while it does not configure production authentication; the API Gateway CORS origin is set to &lt;code&gt;http://localhost&lt;/code&gt;. Review and replace these values with environment-appropriate settings before deployment to a shared or public environment.&lt;/p&gt;

&lt;p&gt;This is what frontend UI should look where users need to provide prompts - &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%2Ff4kb8eymbtxu4pdo4ytv.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%2Ff4kb8eymbtxu4pdo4ytv.png" alt=" " width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: If we want to get list of running of instances name and instance types&lt;/strong&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%2Fk9169ixl1nihzjps65dq.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%2Fk9169ixl1nihzjps65dq.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository Link:&lt;/strong&gt; &lt;a href="https://github.com/dasanirban834/strands-cloudops-agent" rel="noopener noreferrer"&gt;https://github.com/dasanirban834/strands-cloudops-agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;p&gt;NexOps brings selected AWS operational checks into one conversational workflow. This can help engineers get to relevant evidence faster, especially when a question spans multiple services or requires a concise update for teammates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster first-pass investigations:&lt;/strong&gt; Operators can ask a question in plain language and have the agent gather relevant inventory, monitoring, log, and change-history details from supported tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clearer operational summaries:&lt;/strong&gt; Responses can turn service output into readable findings while calling out the region, scope, errors, and uncertainty involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broader access to operational information:&lt;/strong&gt; A conversational interface can make routine checks easier for team members who do not work in every AWS console each day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent review signals:&lt;/strong&gt; Teams can use the same supported checks to spot items such as missing encryption, broad network rules, aged access keys, or low utilization for follow-up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled operational actions:&lt;/strong&gt; Where enabled, lifecycle and Systems Manager actions are bounded by application gates, allowlists, IAM permissions, and AWS prerequisites, with verification available for supported operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployable, modular foundation:&lt;/strong&gt; The separate frontend, backend, tools, and Terraform components provide a basis for adapting the application to an organization's account boundaries and operational practices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These benefits depend on accurate IAM scoping, suitable AWS configuration, and human review. NexOps helps collect and explain evidence; it does not replace incident judgment, security approval, or environment-specific production controls.&lt;/p&gt;

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

&lt;p&gt;NexOps demonstrates how a Strands-based agent can connect a natural-language interface to a focused set of AWS operations. It combines evidence gathering across supported services with readable summaries, while keeping state-changing capabilities explicitly gated.&lt;/p&gt;

&lt;p&gt;The repository provides the core application and infrastructure needed to deploy and extend this approach. Before using it in a shared or production environment, teams should review authentication, CORS, IAM permissions, network access, Bedrock model availability, and mutation settings. With those controls configured for the target environment, NexOps can serve as a practical assistant for routine cloud operations and initial investigation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Master AWS for AI?</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Sat, 07 Nov 2026 11:48:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-master-aws-for-ai-3la4</link>
      <guid>https://dev.to/aws-builders/how-to-master-aws-for-ai-3la4</guid>
      <description>&lt;p&gt;Ciao Amici 👋&lt;/p&gt;

&lt;p&gt;Every hiking trail in the Alps has a map at the trailhead. A good one shows you the huts, the peaks and the river crossings. What it never shows you is the stretch where the path is a loose scree slope, or the hut that closes in October, or the signpost that points you towards the wrong valley. For that, you need someone who has walked it.&lt;/p&gt;

&lt;p&gt;A few days ago a roadmap called "How to Master AWS for AI?" landed in my feed, drawn by Umang Chaudhary (@mlwithumang). It is a lovely trailhead map: one centre (Amazon Bedrock), six branches (Foundations, Data and Compute, Amazon Bedrock, AI Agents, Knowledge and AI Services, Ops and Governance) and a suggested five-step learning path along the bottom. I have built on several of these services myself, so I did the obvious thing. I walked the map against my own scars.&lt;/p&gt;

&lt;p&gt;This is a long article on purpose. It follows the five steps in order, and for each one it gives you three things: what is verifiably true today (as of 30 October 2026), code you can run, and the places where I would move the signposts. Every code sample in this article was executed before I wrote it down, and none of it needs an AWS account or costs a cent. I will tell you exactly how I tested it, and exactly what I did not test, at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in here
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The map, and my three changes to it&lt;/li&gt;
&lt;li&gt;Step 0: money and blast radius (budgets, token ceilings, least privilege, logging)&lt;/li&gt;
&lt;li&gt;Step 1: foundations (reading an AccessDenied properly)&lt;/li&gt;
&lt;li&gt;Step 2: models and prompts (the Converse API, token accounting, stop reasons)&lt;/li&gt;
&lt;li&gt;Step 3: grounding and RAG (S3 Vectors, DynamoDB vectors, and a retrieval test you can run offline)&lt;/li&gt;
&lt;li&gt;Step 4: agents (Strands, AgentCore, and the two rules of distributed systems that break agents)&lt;/li&gt;
&lt;li&gt;Step 5: evaluate and govern (guardrails at the tool boundary, evaluation, audit)&lt;/li&gt;
&lt;li&gt;A failure cheat sheet, a four-week plan, and what I did not verify&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The map in one table
&lt;/h2&gt;

&lt;p&gt;Here is the learning path from the roadmap, in my words:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;The roadmap says&lt;/th&gt;
&lt;th&gt;What it really means&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;Foundations: IAM, KMS, CDK&lt;/td&gt;
&lt;td&gt;Know who is allowed to do what, and keep secrets secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Models and Prompts: Bedrock, Nova&lt;/td&gt;
&lt;td&gt;Call a model and shape what it says&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Grounding and RAG: Knowledge Bases, S3 Vectors&lt;/td&gt;
&lt;td&gt;Make the model answer from your data, not from its imagination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Agents: AgentCore, Strands&lt;/td&gt;
&lt;td&gt;Let the model take actions, safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Evaluate and Govern: Guardrails, Evaluations&lt;/td&gt;
&lt;td&gt;Prove it works, and stop it doing what it should not&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is a sound order for a first pass. My notes are about what a map cannot show, because it is a map and not a diary. Here is the same path with my three changes highlighted. I will explain each one as we go:&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%2F54j1s52bo5obx2vha985.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%2F54j1s52bo5obx2vha985.png" alt="aws-ai-roadmap-figure-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: money and blast radius
&lt;/h2&gt;

&lt;p&gt;The roadmap lists Budgets and Cost Explorer under Core Setup, which I love, but I would pull it out of the pile and put it before everything else. AI services bill by usage, and usage is exactly what an eager experiment or a runaway loop produces. There are four layers of protection, and they do different jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: an alert (slow, but it reaches you).&lt;/strong&gt; AWS Budgets can email you when actual spend passes a percentage of a monthly limit. Here is a budget with two alerts, at 50 and 80 percent of 10 US dollars. I did not guess the structure: I validated it against the SDK's own model for &lt;code&gt;create_budget&lt;/code&gt; (more on that in the testing section).&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;"Budget"&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;"BudgetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai-experiments-monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BudgetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"TimeUnit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MONTHLY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BudgetLimit"&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;"Amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&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;"NotificationsWithSubscribers"&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;"Notification"&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;"NotificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTUAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ComparisonOperator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREATER_THAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ThresholdType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PERCENTAGE"&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;"Subscribers"&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;"SubscriptionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@example.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="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;"Notification"&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;"NotificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTUAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ComparisonOperator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREATER_THAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ThresholdType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PERCENTAGE"&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;"Subscribers"&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;"SubscriptionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@example.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="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;Create it with &lt;code&gt;client.create_budget(AccountId=..., **json.load(open("budget.json")))&lt;/code&gt; using boto3 and the &lt;code&gt;budgets&lt;/code&gt; client. Be honest about what this layer is: an alert, not a brake. It reacts to billing data, which arrives with a delay, so it tells you that you have a problem rather than stopping the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: a brake inside your code.&lt;/strong&gt; The cheapest protection is a ceiling you enforce yourself. This tiny wrapper counts the tokens from every Converse call, refuses to call again once you pass a limit, and writes a ledger line per call so you can see where the tokens went:&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="c1"&gt;# code/token_budget.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A hard token ceiling around Bedrock Converse, with a local usage ledger.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BudgetExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBudget&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;__init__&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;max_tokens&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;ledger_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage.jsonl&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;max_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_tokens&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;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;ledger_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledger_path&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;converse&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;client&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;if&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;used&lt;/span&gt; &lt;span class="o"&gt;&amp;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;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;BudgetExceeded&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="si"&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;used&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&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;max_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&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;converse&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="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&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;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalTokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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;ledger_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&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&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;modelId&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputTokens&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputTokens&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;total_used&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;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the check happens before the call, so one call can overshoot the ceiling by up to one response. That is fine for a safety net. The test that proves it stubs Bedrock locally, makes two calls of 100 tokens against a ceiling of 150, and confirms that the third call is refused and the ledger shows two lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: least privilege.&lt;/strong&gt; Calling Converse requires the &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; permission, and ConverseStream requires &lt;code&gt;bedrock:InvokeModelWithResponseStream&lt;/code&gt;. So a role that may only call one cheap model looks like this:&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;"InvokeOneModelOnly"&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="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModelWithResponseStream"&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:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0"&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;An agent, a notebook or a Lambda that runs under this role cannot suddenly start calling an expensive model, because the policy does not let it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4: know what you are logging.&lt;/strong&gt; Bedrock model invocation logging is off by default. When you turn it on it records the full request and response bodies, token counts and the caller identity for supported calls, and it can write to S3 and to CloudWatch Logs. That is excellent for debugging and for audit, and it also means your log bucket now contains your users' prompts. Encrypt it, restrict access, and set your own retention, because the logs are kept until you delete the configuration. You can tag calls with &lt;code&gt;requestMetadata&lt;/code&gt; in the Converse request so you can filter the logs later.&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%2Fiitn926ya3d8xozhk2xc.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%2Fiitn926ya3d8xozhk2xc.png" alt="aws-ai-roadmap-tokenflow-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more money fact that surprises people: cross-Region inference profiles let a request be served by compute in several Regions. AWS states there is no extra routing cost and that the price is based on the Region from which you call the profile. Geographic profiles use standard pricing and global profiles are described as approximately 10 percent cheaper. They also have a governance side: if your organisation uses Service Control Policies to restrict Regions, the destination Regions of the profile must be allowed, and global profiles need an allowance for &lt;code&gt;aws:RequestedRegion&lt;/code&gt; set to &lt;code&gt;unspecified&lt;/code&gt;. Do not discover this on the day you deploy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rule I follow: if I cannot explain where the money stops, I do not press deploy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: foundations, and how to read an AccessDenied
&lt;/h2&gt;

&lt;p&gt;IAM, KMS, Secrets Manager, VPC endpoints, the CLI and CDK. Nobody gets excited about this branch, and everybody who skips it pays for it later.&lt;/p&gt;

&lt;p&gt;I once wrote a whole article about the fact that the same AWS error can have two different answers, and that guessing which one you are facing is how an afternoon disappears. With AI workloads you meet that early. A model call fails, an agent cannot reach a tool, a Lambda cannot read a bucket, and the message is the same polite "access denied". The skill is to stop guessing and walk a short list, in order:&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%2Fl9gokibcpfpsuxa0lqdl.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%2Fl9gokibcpfpsuxa0lqdl.png" alt="aws-ai-roadmap-accessdenied-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The commands behind that list are boring and worth memorising:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity                      &lt;span class="c"&gt;# who am I, really? (wrong role is the most common cause)&lt;/span&gt;
aws iam simulate-principal-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-source-arn&lt;/span&gt; &amp;lt;role-arn&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action-names&lt;/span&gt; bedrock:InvokeModel &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-arns&lt;/span&gt; arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simulator answers the identity-policy question quickly. If it says allowed and the call still fails, the refusal is coming from somewhere else in the chain: a Service Control Policy or permissions boundary, a resource policy, a KMS key policy, or the Region and model you are actually calling. CloudTrail records the call with its error code and message, which is the receipt you want when two causes look identical.&lt;/p&gt;

&lt;p&gt;On KMS, one design rule from the vector services is worth knowing now. When you create an S3 Vectors bucket you choose its encryption type (SSE-S3 or a KMS key) at creation time, and you cannot change it afterwards. Choosing encryption is a day-one decision, not a later hardening step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: models and prompts
&lt;/h2&gt;

&lt;p&gt;Amazon Bedrock gives you one API over many model families: Amazon Nova, Anthropic Claude, Meta, Mistral and others. For new code, the Converse API is the one to learn, because it has a consistent request and response shape across models that support messages. Here is the minimum:&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;boto3&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_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;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&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;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.nova-micro-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# or an inference profile ID or ARN
&lt;/span&gt;    &lt;span class="n"&gt;system&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;text&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;You are a concise AWS tutor.&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is idempotency, in two sentences?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}],&lt;/span&gt;
    &lt;span class="n"&gt;inferenceConfig&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;maxTokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&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;message&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&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="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                       &lt;span class="c1"&gt;# inputTokens, outputTokens, totalTokens
&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stopReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                   &lt;span class="c1"&gt;# do not ignore this
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three habits are hiding in those lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the stop reason.&lt;/strong&gt; The SDK model lists these values: &lt;code&gt;end_turn&lt;/code&gt;, &lt;code&gt;tool_use&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;, &lt;code&gt;stop_sequence&lt;/code&gt;, &lt;code&gt;guardrail_intervened&lt;/code&gt;, &lt;code&gt;content_filtered&lt;/code&gt;, &lt;code&gt;malformed_model_output&lt;/code&gt;, &lt;code&gt;malformed_tool_use&lt;/code&gt; and &lt;code&gt;model_context_window_exceeded&lt;/code&gt;. Only the first one means "the model finished its thought". A truncated answer (&lt;code&gt;max_tokens&lt;/code&gt;) that your code treats as complete is a bug that looks like a bad model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count tokens on every call.&lt;/strong&gt; The &lt;code&gt;usage&lt;/code&gt; block is your cost meter. Input tokens include everything you send: the system prompt, the conversation history and, later in Step 3, the retrieved passages. Output tokens are what the model writes. That is why a chatty prompt and a large retrieval depth both show up on the bill, and why the ledger from Step 0 pays for itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the knobs that exist.&lt;/strong&gt; Beyond &lt;code&gt;inferenceConfig&lt;/code&gt;, Converse accepts &lt;code&gt;additionalModelRequestFields&lt;/code&gt; for model-specific parameters, &lt;code&gt;requestMetadata&lt;/code&gt; for tagging calls in your logs, &lt;code&gt;guardrailConfig&lt;/code&gt; for attaching a guardrail, and &lt;code&gt;serviceTier&lt;/code&gt;, whose documented values are &lt;code&gt;reserved&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt; and &lt;code&gt;flex&lt;/code&gt;. Prompt caching works by adding &lt;code&gt;cachePoint&lt;/code&gt; blocks, and which blocks a model accepts depends on the model. A detail from the docs that bites people: if you call a prompt from Prompt management, you cannot also pass &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;inferenceConfig&lt;/code&gt;, &lt;code&gt;toolConfig&lt;/code&gt; or &lt;code&gt;additionalModelRequestFields&lt;/code&gt;, because the managed prompt owns them.&lt;/p&gt;

&lt;p&gt;Tool use, which Step 4 builds on, is part of the same API. You describe tools in &lt;code&gt;toolConfig&lt;/code&gt;, the model answers with &lt;code&gt;stopReason&lt;/code&gt; set to &lt;code&gt;tool_use&lt;/code&gt; and a &lt;code&gt;toolUse&lt;/code&gt; block (an id, a name and an input), you run the tool, and you send the outcome back as a &lt;code&gt;toolResult&lt;/code&gt; for that id with a status of success or error. Every agent framework is, underneath, a loop around that exchange.&lt;/p&gt;

&lt;p&gt;The thing I would add here is a habit, not a service. &lt;strong&gt;A model that sounds confident is not a model that is right.&lt;/strong&gt; I built a small game, gaslight-bench, to find out whether a model can be talked into agreeing with false statements about AWS. The game is playful, but the question behind it is serious: if a model can be pushed off the facts by a confident voice, its own tone can never be your evidence. And the first piece of evidence you can build is tiny: five questions you know the answers to, run every time you change a prompt or a model. That is where evaluation starts, and it starts here, not in Step 5. We will turn it into a real harness in Step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: grounding and RAG
&lt;/h2&gt;

&lt;p&gt;If you want the model to answer from your documents, the system has two halves, and it helps to keep them separate in your head:&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%2Fncquhljrfydoa45ibj3y.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%2Fncquhljrfydoa45ibj3y.png" alt="aws-ai-roadmap-ragpipeline-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ingest half runs when your documents change: split them into chunks, turn each chunk into an embedding (a list of numbers), and store the embedding with the chunk's text and some metadata. The query half runs on every question: embed the question, fetch the nearest chunks, put them in the prompt, and ask the model to answer from them. Most RAG problems are in the middle: the right chunk never comes back, so the model never sees the answer, and then it improvises.&lt;/p&gt;

&lt;p&gt;The roadmap lists four places to keep embeddings: S3 and S3 Vectors, OpenSearch Serverless, DynamoDB vector search and Aurora pgvector. Two of those have changed a lot recently, so here is what I could verify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon S3 Vectors&lt;/strong&gt; became generally available on 2 December 2025. AWS states up to 2 billion vectors per index and up to 10,000 indexes per vector bucket, with queries around 100 ms for frequent use and under a second for infrequent ones, and claims up to 90 percent lower total cost for uploading, storing and querying vectors. It plugs into Bedrock Knowledge Bases, and into OpenSearch for hybrid search. The SDK model tells you the practical constraints: the only data type is &lt;code&gt;float32&lt;/code&gt;, the distance metric is &lt;code&gt;cosine&lt;/code&gt; or &lt;code&gt;euclidean&lt;/code&gt;, and the index name, dimension, distance metric and non-filterable metadata keys are fixed when you create the index. That last point is the one to plan around: if you change your embedding model later, the dimension may change, and you will re-embed and re-index everything into a new index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon DynamoDB vector search&lt;/strong&gt; became generally available on 5 August 2026. You store embeddings next to your normal item attributes, define a vector index (at table creation through &lt;code&gt;VectorIndexes&lt;/code&gt;, or later through &lt;code&gt;VectorIndexUpdates&lt;/code&gt;), and search with the &lt;code&gt;SearchVectors&lt;/code&gt; operation, which takes the index name, the query vector, a &lt;code&gt;TopK&lt;/code&gt; and an optional condition expression to filter on item attributes. The distance functions are &lt;code&gt;COSINE&lt;/code&gt;, &lt;code&gt;DOT_PRODUCT&lt;/code&gt; and &lt;code&gt;EUCLIDEAN&lt;/code&gt;, and AWS states a target of single-digit millisecond latency at 99 percent plus recall. One detail trips people up: DynamoDB does not create the embeddings for you. You generate them with a model of your choice (a Bedrock model works) and store them. Write-ups of the launch also say that vector indexes require on-demand capacity and cap the number of results returned per search, so read the current limits page before you design around either.&lt;/p&gt;

&lt;p&gt;Here is the same embedding stored both ways. The calls are shaped exactly to the SDK's current models (I validated every parameter offline, see the testing section), and the &lt;code&gt;embed&lt;/code&gt; function is the only part that spends money:&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="c1"&gt;# code/vector_stores.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Two ways to store the same embedding on AWS. Calls are shaped to the current SDK models.

Not run against a live account in my tests: they are validated offline (see test_vectors.py).
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.titan-embed-text-v2:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# check the model card for the request body you use
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&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;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Real embedding call (costs a little per token). Swap in any embedding model.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normalize&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;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&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="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="c1"&gt;# ---- S3 Vectors ----------------------------------------------------------------
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;s3_vectors_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_vector_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorBucketName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;vectorBucketName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dataType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimension&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distanceMetric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# Big text you only want back, never filter on: mark it non-filterable (immutable later).
&lt;/span&gt;        &lt;span class="n"&gt;metadataConfiguration&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;nonFilterableMetadataKeys&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_text&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;def&lt;/span&gt; &lt;span class="nf"&gt;s3_vectors_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;items: [(key, embedding, {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ..., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ...}), ...]&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_vectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;vectorBucketName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vectors&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;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;md&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&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;s3_vectors_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;kw&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;filter&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;topic&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;topic&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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_vectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;vectorBucketName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topK&lt;/span&gt;&lt;span class="o"&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;queryVector&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;float32&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_embedding&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;returnMetadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;returnDistance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;,&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;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vectors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# each has key, distance, metadata
&lt;/span&gt;

&lt;span class="c1"&gt;# ---- DynamoDB vector search ----------------------------------------------------
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ddb_create_table_with_vector_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BillingMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# vector indexes need on-demand capacity (per AWS write-ups; check docs)
&lt;/span&gt;        &lt;span class="n"&gt;AttributeDefinitions&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;AttributeName&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;pk&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;AttributeType&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;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;KeySchema&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;AttributeName&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;pk&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;KeyType&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;HASH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;VectorIndexes&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;IndexName&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;by-embedding&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;VectorAttribute&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&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;embedding&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;Dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DistanceFunction&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;COSINE&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;Projection&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ProjectionType&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;ALL&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="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ddb_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search_vectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IndexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;by-embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TopK&lt;/span&gt;&lt;span class="o"&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;SearchVector&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;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;],&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;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SearchResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How do I choose? Check the current documentation for limits and pricing, then pick the boring option that matches the shape of your data:&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%2Fwu9ebz58krtybeo027d6.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%2Fwu9ebz58krtybeo027d6.png" alt="aws-ai-roadmap-vectors-aws" width="800" height="450"&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;If your situation is...&lt;/th&gt;
&lt;th&gt;I would start with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lots of documents, queries are occasional, cost matters most&lt;/td&gt;
&lt;td&gt;S3 Vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your app already lives in DynamoDB and the vectors belong to the same items (agent memory, product similarity)&lt;/td&gt;
&lt;td&gt;DynamoDB vector search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need rich search: hybrid keyword plus vector, ranking, analytics&lt;/td&gt;
&lt;td&gt;OpenSearch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your data is relational and you already run Postgres&lt;/td&gt;
&lt;td&gt;Aurora with pgvector&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The test that matters more than the database
&lt;/h3&gt;

&lt;p&gt;Choosing a store is the easy decision. The hard question is "does the right chunk come back?", and you can answer it with a page of Python, offline, before you spend anything. A retrieval test needs a golden set (questions with the chunk that should answer each one) and two numbers: recall@k (how often the right chunk is in the top k) and MRR (how high it ranks, on average).&lt;/p&gt;

&lt;p&gt;Here is the whole harness. The embedding in it is a deliberately simple hashed bag of words, a stand-in so the harness runs anywhere; swap &lt;code&gt;embed()&lt;/code&gt; for a real model and keep everything else:&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="c1"&gt;# code/retrieval_eval.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A retrieval test you can run with no AWS account: recall@k and MRR over a golden set.

The embedding here is a hashed bag of words, a stand-in so the harness runs anywhere.
Swap embed() for a real model call (see the Bedrock version in the article) and keep the rest.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;zlib&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;CHUNKS&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;c1&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;S3 event notifications are delivered at least once so handlers must tolerate duplicates&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;c2&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;Lambda retries failed asynchronous invocations up to two more times by default&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;c3&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;The sequencer field in an S3 event orders events for the same object key&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;c4&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;Bedrock Guardrails check model input and model output but not tool parameters by default&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;c5&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;AgentCore Gateway turns APIs Lambda functions and MCP servers into tools for agents&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;c6&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;AgentCore Memory keeps short term session events and long term extracted records&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;c7&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;S3 Vectors stores embeddings in vector buckets and queries them with cosine or euclidean distance&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;c8&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;DynamoDB vector search stores embeddings next to item attributes and you generate them yourself&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;c9&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;Model invocation logging is off by default and records prompts and responses when enabled&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;c10&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;AWS Budgets can alert you when actual spend passes a percentage of your monthly limit&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;c11&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;Cross Region inference profiles route requests to compute in several Regions&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;c12&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;Strands hooks run before a tool call after a tool call and before a model invocation&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="n"&gt;GOLDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;  &lt;span class="c1"&gt;# (question, id of the chunk that should come back)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how many times does lambda retry an async invocation&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;c2&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;can an s3 notification arrive twice&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;c1&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how do i order s3 events for one key&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;c3&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do guardrails inspect tool parameters&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;c4&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;which agentcore piece exposes lambda functions as tools&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;c5&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;where do agents keep long term memory&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;c6&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;which distance metrics does s3 vectors support&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;c7&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;who creates the embeddings for dynamodb vector search&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;c8&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is bedrock prompt logging enabled by default&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;c9&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how do i get an alert at 80 percent of my monthly spend&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;c10&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;what runs just before a tool call in strands&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;c12&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send my traffic to another region automatically&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;c11&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop paying for something i forgot to turn off&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;c10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# real paraphrase: no shared words, so a lexical embedder should miss
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;STOP&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;a&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;an&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;the&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;of&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;in&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;to&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;is&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;are&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;do&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;does&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;how&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;i&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;my&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;for&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;and&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;or&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;on&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;at&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;it&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;by&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;up&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;you&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;can&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;which&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;what&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;where&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;who&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;than&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;so&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;but&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;not&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;when&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;DIM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DIM&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;tok&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[a-z0-9]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&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;tok&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STOP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;zlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crc32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;DIM&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&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="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&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;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHUNKS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHUNKS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;k_values&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;rr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;misses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&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;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GOLDEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;k_values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;misses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GOLDEN&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recall@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&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="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;k_values&lt;/span&gt;&lt;span class="p"&gt;}&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;mrr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;misses&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="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;misses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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="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;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;misses&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MISS  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;  wanted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;got&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it prints this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recall@1   0.85
recall@3   0.92
mrr        0.89
MISS  'stop paying for something i forgot to turn off'  wanted c10  got ['c9', 'c1', 'c3']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the miss, because the miss is the point. The question "stop paying for something I forgot to turn off" is about AWS Budgets, but it shares no words with the chunk about budgets, so a lexical embedder cannot find it. A real embedding model closes much of that gap, and hybrid search (keyword plus vector) closes more, but you will not know how much until you measure. Without the golden set you would have shipped a system that sounds right and silently misses a whole class of questions. With it, the same twelve lines of output tell you whether a new chunk size, a new embedding model or a new filter made things better or worse. Treat it like a unit test for retrieval, and put it in CI.&lt;/p&gt;

&lt;p&gt;Two practical notes. First, chunk size is a trade: small chunks are precise but lose context, large chunks carry context but dilute the match and cost more input tokens when you stuff them in the prompt. Test two or three sizes against your golden set instead of copying someone's default. Second, use metadata filters (S3 Vectors takes a &lt;code&gt;filter&lt;/code&gt; on query, DynamoDB takes a condition expression) to narrow by tenant, language or document type before similarity does its work. It is cheaper and more accurate than hoping the nearest neighbour is also the right tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: agents, and where reality arrives
&lt;/h2&gt;

&lt;p&gt;This is the branch I know best, because I lived it. The Amazon Bedrock AgentCore family went generally available in October 2025 and covers the pieces you would otherwise build yourself:&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%2Fj1odmwa20bwlx1zukon9.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%2Fj1odmwa20bwlx1zukon9.png" alt="aws-ai-roadmap-agentcoremap-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Runtime hosts your agent code in a serverless environment with each session isolated. Memory keeps short-term session events and long-term extracted records. Gateway turns APIs, Lambda functions and MCP servers into tools behind one endpoint. Identity gives agents workload identities and a token vault for OAuth flows. Observability sends metrics, traces and logs to CloudWatch with OpenTelemetry export. Code Interpreter and Browser run sandboxed code and managed browser sessions. Policy checks each tool call at the Gateway against Cedar rules. Around it sit the builders the roadmap lists: the Strands Agents SDK, Kiro and Amazon Quick.&lt;/p&gt;

&lt;h3&gt;
  
  
  An agent in a dozen lines, and what the framework is doing
&lt;/h3&gt;

&lt;p&gt;With the Strands Agents SDK, a first agent is genuinely small. And because Strands has a provider for Ollama (&lt;code&gt;pip install 'strands-agents[ollama]'&lt;/code&gt;), you can build and test the whole loop on a local model with no AWS bill at all:&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;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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models.ollama&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaModel&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;get_invoice_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;str&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 total for an invoice.&lt;/span&gt;&lt;span class="sh"&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;INV-1&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;42.00&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;INV-2&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;17.50&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;invoice_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;not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# ollama pull llama3.1 first
&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="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;get_invoice_total&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the total of invoice INV-2?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind that one call is the loop from Step 2: the model asks for &lt;code&gt;get_invoice_total&lt;/code&gt;, the framework runs it, feeds the result back, and the model writes the answer. When the model is good enough, you change one line to use a Bedrock model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting it: the AgentCore CLI changed under me
&lt;/h3&gt;

&lt;p&gt;One thing I would have been bitten by if I had copied an older tutorial: the Python starter toolkit CLI for AgentCore (the &lt;code&gt;agentcore configure&lt;/code&gt; and &lt;code&gt;agentcore launch&lt;/code&gt; flow) now prints a notice that it is no longer supported and points to a new AgentCore CLI that you install from npm (&lt;code&gt;npm install -g @aws/agentcore&lt;/code&gt;). I installed the new one (version 1.0.1) and read its help. It scaffolds a project from templates (for example &lt;code&gt;agent-python-strands&lt;/code&gt;, a minimal Python template, LangChain, TypeScript and MCP server templates), runs agents locally with &lt;code&gt;agentcore dev&lt;/code&gt;, and ships with &lt;code&gt;deploy&lt;/code&gt;, &lt;code&gt;invoke&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;logs&lt;/code&gt; and &lt;code&gt;traces&lt;/code&gt; commands, plus an &lt;code&gt;eval&lt;/code&gt; command group that we will meet in Step 5. The workflow is therefore:&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
agentcore create &lt;span class="nt"&gt;--name&lt;/span&gt; robot-demo &lt;span class="nt"&gt;--template&lt;/span&gt; agent-python-strands
agentcore dev          &lt;span class="c"&gt;# runs the agent locally, with local traces&lt;/span&gt;
&lt;span class="c"&gt;# agentcore deploy     # this one builds and deploys to AWS: it is where the bill starts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice where the money starts. Everything up to &lt;code&gt;dev&lt;/code&gt; is local. &lt;code&gt;deploy&lt;/code&gt; creates real resources in your account, so keep it behind the budget and the least-privilege role from Step 0, and delete what you deploy when the experiment is over.&lt;/p&gt;

&lt;p&gt;If you want to see the runtime contract for yourself without deploying, the SDK's app class is small. This entrypoint, tested through a local test client, answers &lt;code&gt;POST /invocations&lt;/code&gt; with your return value and &lt;code&gt;GET /ping&lt;/code&gt; with a health status:&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="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;@app.entrypoint&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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;prompt&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;Hello&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;result&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;echo: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&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;# replace with: agent(prompt)
&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I posted &lt;code&gt;{"prompt": "ping"}&lt;/code&gt; to &lt;code&gt;/invocations&lt;/code&gt; I got &lt;code&gt;{"result": "echo: ping"}&lt;/code&gt; with a 200, and &lt;code&gt;/ping&lt;/code&gt; answered &lt;code&gt;{"status":"Healthy", ...}&lt;/code&gt;. That is the whole contract your agent code has to honour.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two rules that break agents
&lt;/h3&gt;

&lt;p&gt;An agent is a distributed system with a language model in the middle, and distributed systems have rules that do not care how clever your prompt is. Two of them cost me the most, and the roadmap does not mention either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: triggers deliver at least once.&lt;/strong&gt; S3 event notifications are designed to be delivered at least once, and Lambda retries failed asynchronous invocations up to two more times by default. So if your tool charges a customer, writes to a ledger or sends an email, it will eventually do it twice unless you made it safe to repeat.&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%2F1flwa182wawess4ej3p6.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%2F1flwa182wawess4ej3p6.png" alt="aws-ai-roadmap-repeat-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is an old bookkeeping idea: write down a key before you act, and check it first. A naive version (insert the key, then act) has a hole that readers pointed out after my first idempotency article: what happens when the first worker crashes after claiming the key but before finishing? The key says "in progress" forever, and the work never completes. The robust version gives each claim a lease and a fencing check. Here it is as one small module on top of a single DynamoDB table:&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="c1"&gt;# code/idempotency.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Idempotent execution with a lease, backed by one DynamoDB table.

Table: partition key &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (string). Enable TTL on &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; to clean up.
States: INPROGRESS (has an owner token and a lease) and DONE (has a result).
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Another worker holds a live lease on this key. Let the retry come back later.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LostLease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Our lease expired and someone else took over. Do not record our result.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_failed_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ClientError&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;bool&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&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;Code&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyStore&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;__init__&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;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lease_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retention_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&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;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&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;lease&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lease_seconds&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;retention&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retention_seconds&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;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claim&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;key&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 (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, token) if we may do the work, or (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, result) if it is already done.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&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="nf"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Item&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;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;INPROGRESS&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;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lease_until&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&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;lease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&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;retention&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="c1"&gt;# New key, or an abandoned claim whose lease has run out.
&lt;/span&gt;                &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(pk) OR (#s = :inprog AND lease_until &amp;lt; :now)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ExpressionAttributeNames&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;#s&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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;ExpressionAttributeValues&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;:inprog&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;INPROGRESS&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;:now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;_failed_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="n"&gt;item&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&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;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ConsistentRead&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;Item&lt;/span&gt;&lt;span class="sh"&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;item&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&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;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;complete&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Record the result, but only if we still own the claim (the fencing check).&lt;/span&gt;&lt;span class="sh"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Key&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;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET #s = :done, #r = :r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#o = :t AND #s = :inprog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ExpressionAttributeNames&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;#s&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;status&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;#r&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;result&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;#o&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;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;ExpressionAttributeValues&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;:done&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;DONE&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;:inprog&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;INPROGRESS&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;:t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_failed_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LostLease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Give the key back after a failure so the next retry can run immediately.&lt;/span&gt;&lt;span class="sh"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Key&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;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#o = :t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ExpressionAttributeNames&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;#o&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;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;ExpressionAttributeValues&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;:t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;_failed_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IdempotencyStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run fn() at most once per key. fn must return a string (store JSON if you need more).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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;claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&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="n"&gt;value&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&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="p"&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;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&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;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk through the four situations, because all four are tested. (1) The same key twice runs the side effect once, and the second call returns the stored result. (2) A second worker arriving while the first holds a live lease gets &lt;code&gt;InProgress&lt;/code&gt;, so it backs off and the retry comes later. (3) If the first worker stalls past its lease, a second worker can take over, and when the stalled worker finally wakes up and tries to record its result, the conditional write refuses it with &lt;code&gt;LostLease&lt;/code&gt;. That refusal is the fencing check. (4) If the work fails, the key is released, so the retry can run immediately instead of waiting for the lease to expire.&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%2Fwvu1ybql7sool9ryyo4s.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%2Fwvu1ybql7sool9ryyo4s.png" alt="aws-ai-roadmap-lease-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the honest limit, which I would rather you hear from me than discover in production. The fencing check protects your record. It does not magically undo a side effect that the stalled worker already performed in the outside world. If the stalled worker already charged the card, the new worker may charge it again. The only complete answer is to pass the idempotency key to the system that performs the side effect (a payment provider's idempotency key, for example), so the duplicate is rejected there too. Think of the lease as protection for your own ledger, and the downstream key as protection for the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: events do not arrive in order.&lt;/strong&gt; Amazon S3's own documentation says event notifications are not guaranteed to arrive in the order the events occurred. For object creation and deletion events it gives you a tool: the &lt;code&gt;sequencer&lt;/code&gt; field, a hexadecimal string that you can compare for events on the same object key (and only the same key). The documented comparison has a trap: the strings can differ in length, so you left-pad the shorter one with zeros and then compare lexicographically. Compare them raw and you get a wrong answer: as text, &lt;code&gt;10&lt;/code&gt; sorts before &lt;code&gt;F&lt;/code&gt;, although in hex &lt;code&gt;10&lt;/code&gt; (16) is later than &lt;code&gt;F&lt;/code&gt; (15). Here is a guard that only applies an event if it is newer than the one you stored, using a conditional write so it is safe even when two events race:&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="c1"&gt;# code/ordering.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Drop stale events: only apply an S3 event if its sequencer is newer than the one we stored.

AWS guidance: the sequencer is hex, only comparable for the same object key, and values can
differ in length, so left-pad the shorter one with zeros and compare as strings.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="n"&gt;WIDTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;  &lt;span class="c1"&gt;# comfortably longer than real sequencers
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sequencer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;str&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;sequencer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;zfill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WIDTH&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;is_later&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;str&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;str&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;normalise&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;normalise&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_if_newer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequencer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="nb"&gt;str&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write payload only when this event is newer. Returns False for a stale event.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sequencer&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Key&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;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;object_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET seq = :s, payload = :p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(seq) OR seq &amp;lt; :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;ExpressionAttributeValues&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;: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;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:p&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&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;Code&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConditionalCheckFailedException&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="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test applies an event, then a newer one, then a stale one that arrives late, and confirms that the stale one is dropped and the stored payload is still the newest. It also asserts the &lt;code&gt;10&lt;/code&gt; versus &lt;code&gt;F&lt;/code&gt; trap, so the padding cannot be removed by accident.&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%2Frla3e09209kb73u160ii.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%2Frla3e09209kb73u160ii.png" alt="aws-ai-roadmap-ordering" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If I could add one signpost to Step 4 it would read: &lt;strong&gt;before you give an agent tools, make the tools safe to repeat and safe to reorder.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: evaluate and govern
&lt;/h2&gt;

&lt;p&gt;The last step has two halves, and they are different jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Governing: guardrails stop at the model call
&lt;/h3&gt;

&lt;p&gt;Bedrock Guardrails check what goes into a model and what comes out of it. An important nuance, which AWS spells out in its own security guidance: that boundary is the model call. Tool parameters, external data and traffic between agents sit outside it.&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%2Fkxwvxuk58s2dngffc5at.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%2Fkxwvxuk58s2dngffc5at.png" alt="aws-ai-roadmap-boundary-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Strands Agents SDK lets you extend the check to those moments with hooks. In the version I installed (strands-agents 1.62.0), a &lt;code&gt;BeforeToolCallEvent&lt;/code&gt; carries the &lt;code&gt;tool_use&lt;/code&gt; (name and input) and a &lt;code&gt;cancel_tool&lt;/code&gt; field: set it to a string and the tool is skipped and the string goes back to the model. An &lt;code&gt;AfterToolCallEvent&lt;/code&gt; carries the &lt;code&gt;result&lt;/code&gt;, which you may replace. And the &lt;code&gt;ApplyGuardrail&lt;/code&gt; operation lets you run a guardrail on any text yourself, with &lt;code&gt;source&lt;/code&gt; set to &lt;code&gt;INPUT&lt;/code&gt; or &lt;code&gt;OUTPUT&lt;/code&gt;, returning an &lt;code&gt;action&lt;/code&gt; of &lt;code&gt;NONE&lt;/code&gt; or &lt;code&gt;GUARDRAIL_INTERVENED&lt;/code&gt;. Put those together and you get a guardrail on tools:&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="c1"&gt;# code/guardrail_hook.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extend Bedrock Guardrails to tool calls in a Strands agent.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.hooks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AfterToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BeforeToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HookProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HookRegistry&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuardrailHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HookProvider&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;__init__&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;bedrock_runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guardrail_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guardrail_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DRAFT&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrock_runtime&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;gid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guardrail_id&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;gver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guardrail_version&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_hooks&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;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HookRegistry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BeforeToolCallEvent&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;before_tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AfterToolCallEvent&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;after_tool&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;_blocked&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;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&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;apply_guardrail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;guardrailIdentifier&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;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;guardrailVersion&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;gver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# "INPUT" or "OUTPUT"
&lt;/span&gt;            &lt;span class="n"&gt;content&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;text&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="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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}}],&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;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GUARDRAIL_INTERVENED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;before_tool&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BeforeToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_blocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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;tool_use&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])):&lt;/span&gt;
            &lt;span class="c1"&gt;# Setting cancel_tool skips the tool; the string goes back to the model.
&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;cancel_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocked by guardrail. Do not retry this call.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;after_tool&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AfterToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&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="nf"&gt;join&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="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;text&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&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;result&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;content&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;text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_blocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OUTPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&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;result&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;toolUseId&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toolUseId&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;status&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;error&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;content&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tool result withheld by guardrail.&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="c1"&gt;# Usage:
#   from strands import Agent
#   agent = Agent(tools=[...], hooks=[GuardrailHook(boto3.client("bedrock-runtime"), "gr-id", "1")])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test registers the hook with a Strands hook registry, then feeds it fake events: a tool input containing a sensitive marker gets cancelled, a clean one passes, and a tool result containing the marker is replaced by an error result. It uses a stub in place of Bedrock, so it proves the wiring, not the guardrail's judgement. The judgement, what your guardrail actually blocks, is the part you must test with your own data.&lt;/p&gt;

&lt;p&gt;Three other pieces belong in the governing kit. &lt;strong&gt;AgentCore Policy&lt;/strong&gt; checks each tool call at the Gateway against Cedar rules, which is the right tool when a rule must be a hard rule rather than a hopeful instruction in a prompt. &lt;strong&gt;Automated Reasoning checks&lt;/strong&gt;, part of Guardrails, check model answers against policies you define, which suits rule-heavy domains. And for the paper trail, &lt;strong&gt;CloudTrail&lt;/strong&gt; records who called what, while the &lt;strong&gt;invocation logging&lt;/strong&gt; from Step 0 records what was said.&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluating: evidence, not vibes
&lt;/h3&gt;

&lt;p&gt;Bedrock Evaluations and AgentCore Observability are on the roadmap. AgentCore Evaluations was announced in preview in December 2025, and the new AgentCore CLI I installed today has an &lt;code&gt;eval&lt;/code&gt; command group with subcommands for evaluators, online evaluation configs, datasets, batch evaluations, on-demand evaluation of existing sessions, and A/B tests. Preview and general availability change, so check the current status before you plan around it. The shape of the idea is stable, though, and you can practise it for free:&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%2Fcxi7d756k60rw0p48ygi.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%2Fcxi7d756k60rw0p48ygi.png" alt="aws-ai-roadmap-evalloop-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A golden set of cases, a runner that executes them, graders that score each result, and a gate that fails the build when a score drops. You have already seen the smallest version: the retrieval harness in Step 3. The tests in this article are the same idea applied to reliability: the idempotency test replays the same event twice, the ordering test delivers events backwards, and the budget test tries to overspend. For agents, add a red-team list of nasty inputs. A good one covers instructions hidden in tool output, requests to reveal the system prompt, malformed arguments and attempts to talk the agent into skipping a check. I once spent a whole article attacking my own agent with chaos tests and injected instructions, and the lesson was simple: attack your work before someone else does, and do not wait for the last step of a learning path to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  A failure cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;First thing to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Customer charged twice, logs spotless&lt;/td&gt;
&lt;td&gt;At-least-once delivery or an async retry&lt;/td&gt;
&lt;td&gt;An idempotency key checked before the side effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ledger shows an old value after a newer one&lt;/td&gt;
&lt;td&gt;Events arrived out of order&lt;/td&gt;
&lt;td&gt;Sequencer compare, conditional write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work never finishes after a crash&lt;/td&gt;
&lt;td&gt;A claim with no lease&lt;/td&gt;
&lt;td&gt;Lease expiry on the claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer sounds right but cites nothing real&lt;/td&gt;
&lt;td&gt;The right chunk was never retrieved&lt;/td&gt;
&lt;td&gt;recall@k on a golden set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer cut off mid-sentence&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max_tokens&lt;/code&gt; stop reason treated as complete&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stopReason&lt;/code&gt; on every call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bill higher than expected&lt;/td&gt;
&lt;td&gt;Large history or retrieval depth, a loop&lt;/td&gt;
&lt;td&gt;The token ledger, then &lt;code&gt;usage&lt;/code&gt; per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AccessDenied that policy simulator says is allowed&lt;/td&gt;
&lt;td&gt;SCP, resource policy, KMS, Region or model&lt;/td&gt;
&lt;td&gt;CloudTrail error message and the Step 1 list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool does something it should not&lt;/td&gt;
&lt;td&gt;Guardrails stop at the model call&lt;/td&gt;
&lt;td&gt;A hook on tool calls, or Policy at the Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where I would move the signposts
&lt;/h2&gt;

&lt;p&gt;Here is my honest list of changes to the map. They are small, and they are all about order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start evaluating in Step 2, not Step 5.&lt;/strong&gt; The day you call a model, write down five questions you know the answer to and check them. That tiny habit grows into an evaluation suite. Waiting until the end means evaluating a finished building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put cost controls at step zero.&lt;/strong&gt; A budget, a token ceiling and a one-model role are cheaper than any lesson.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a reliability step between 3 and 4.&lt;/strong&gt; Idempotency, leases, retries and ordering. It is not glamorous and it is where production breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the map as a menu, not a checklist.&lt;/strong&gt; Textract, Polly, Comprehend and Rekognition are excellent, and you do not need them to learn AI on AWS. Learn them when a real problem asks for them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A four week plan that respects your wallet
&lt;/h2&gt;

&lt;p&gt;If I were starting again, this is how I would spend four weeks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;The small proof you build&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;IAM, budgets, one model call&lt;/td&gt;
&lt;td&gt;A budget alert, and a script that calls one Bedrock model through the token ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;A tiny knowledge base over twenty of your own documents, and five questions you can grade with recall@k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;One agent with two tools&lt;/td&gt;
&lt;td&gt;Tools that are safe to repeat and reorder, and a test that replays the same event twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Guardrails and evaluation&lt;/td&gt;
&lt;td&gt;A guardrail on tool calls, plus a red-team list of ten nasty inputs wired into the build&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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%2Fsv7i5r2wjrogm71m61ib.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%2Fsv7i5r2wjrogm71m61ib.png" alt="aws-ai-roadmap-plan-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice what the table does not contain: a big deployment. Each week ends with something small, local where possible, and provable. If you want a "done when" for each week, it is this: week 1 is done when the alert exists and the ceiling stops a loop; week 2 when a miss in your golden set makes you change something; week 3 when the replay test passes; week 4 when a red-team input fails the build until you fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested this, and what I did not
&lt;/h2&gt;

&lt;p&gt;I would rather tell you the limits of my testing than let you assume them.&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%2Fwvbckxhf1t53trk73kwc.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%2Fwvbckxhf1t53trk73kwc.png" alt="aws-ai-roadmap-tested-aws" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I ran.&lt;/strong&gt; I installed boto3 1.43.122, moto 5.2.5 (a local stand-in for DynamoDB), numpy, strands-agents 1.62.0, bedrock-agentcore 1.25.2 and the new AgentCore CLI 1.0.1, and ran: the idempotency lease (four situations), the ordering guard, the token ceiling (against a stubbed Bedrock client), the budget JSON (validated through the SDK's request validation), the retrieval harness, the guardrail hook (against a stubbed Bedrock client and a real Strands hook registry), the vector store calls (parameter validation against the SDK's models for S3 Vectors and DynamoDB), and the AgentCore app routes (through a local test client). They all pass, and they are in the companion folder with a &lt;code&gt;README&lt;/code&gt; and a single command to run them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did not run.&lt;/strong&gt; None of this touched a real AWS account, because I do not spend money on experiments I can prove locally. So I have not measured real Bedrock latency or cost, I have not run &lt;code&gt;embed()&lt;/code&gt; against a real embedding model (check the model card for the exact request body of the model you pick), I have not exercised a real guardrail's judgement, and I have not deployed anything with &lt;code&gt;agentcore deploy&lt;/code&gt;. Facts about services (limits, dates, availability) come from AWS announcements and documentation, and a few details (the on-demand capacity requirement and result cap for DynamoDB vector search) come from third-party write-ups of AWS's launch, so I flagged them where they appear. Services change: verify limits and pricing against the current documentation before you build on them.&lt;/p&gt;

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

&lt;p&gt;The roadmap is a very good map. Maps are made by people who stand back far enough to see the whole mountain, and that is a gift. What I tried to add here is the view from the path: the scree, the closed hut, the sign that points the wrong way, and a few tools you can pick up for free. Use the map to know where you are going, and use your own mistakes (cheaply, on a small budget) to learn how to get there.&lt;/p&gt;

&lt;p&gt;If you want to see the terrain I was talking about, here are the stories behind the field notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moving the Robot Accountant to AgentCore: &lt;a href="https://dev.to/aws-builders/my-robot-accountant-got-an-eviction-notice-so-we-moved-migrating-from-bedrock-agents-classic-to-4bjo"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://builder.aws.com/content/3HG3hliZWmkB9NxPWzRwDT3C4EQ/my-robot-accountant-got-an-eviction-notice-so-we-moved-migrating-from-bedrock-agents-classic-to-agentcore" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The IAM error that has two answers: &lt;a href="https://builder.aws.com/content/3Hiany3RvGw3r2XwykfrHrzCHSJ/same-aws-error-two-answers-i-built-a-tool-that-stops-guessing-at-iam" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Out-of-order events and AI agents: &lt;a href="https://builder.aws.com/content/3JxsTPAOvP7Vu35C69M69oLtIg9/out-of-order-events-break-your-ai-agent-on-aws" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Breaking my own agent on purpose: &lt;a href="https://builder.aws.com/content/3JdQRGZGTyqjiwtn0N97nUu9U2Y/i-tried-to-break-my-robot-accountant-so-nobody-else-could" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The ledger that charged twice: &lt;a href="https://dev.to/aws-builders/the-ledger-that-charged-twice-s3-lambda-idempotency-3d4g"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://builder.aws.com/content/3L0uEMyimfBbcXAAaUJI1KSzNHz/the-ledger-that-charged-twice-s3-lambda-idempotency" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Can you gaslight an AI about AWS?: &lt;a href="https://dev.to/aws-builders/can-you-gaslight-an-ai-about-aws-i-built-a-game-to-find-out-4o03"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://github.com/mursalfk/gaslight-bench" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the code from this article, with its tests, is in the companion repository: &lt;a href="https://github.com/mursalfk/aws-for-ai-field-guide" rel="noopener noreferrer"&gt;https://github.com/mursalfk/aws-for-ai-field-guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What is the one step of this path you wish somebody had warned you about?&lt;/p&gt;

&lt;p&gt;Ciao! 👋&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Amazon S3 Vectors generally available: &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-s3-vectors-generally-available/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-s3-vectors-generally-available/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon DynamoDB vector search generally available: &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-dynamodb-vector-search/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-dynamodb-vector-search/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AgentCore Policy and Evaluations (preview announcement): &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-bedrock-agentcore-policy-evaluations-preview" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-bedrock-agentcore-policy-evaluations-preview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Extending Bedrock Guardrails to tool interactions with Strands: &lt;a href="https://aws.amazon.com/blogs/security/extend-amazon-bedrock-guardrails-to-tool-interactions-using-the-strands-agents-sdk/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/security/extend-amazon-bedrock-guardrails-to-tool-interactions-using-the-strands-agents-sdk/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Automated Reasoning checks in Bedrock Guardrails: &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-automated-reasoning-checks.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-automated-reasoning-checks.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Converse API and its permissions: &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cross-Region inference and inference profiles: &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model invocation logging: &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon S3 event message structure (the sequencer field): &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Strands Agents hooks: &lt;a href="https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/hooks/" rel="noopener noreferrer"&gt;https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/hooks/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Strands Ollama model provider: &lt;a href="https://strandsagents.com/latest/documentation/docs/user-guide/concepts/model-providers/ollama/" rel="noopener noreferrer"&gt;https://strandsagents.com/latest/documentation/docs/user-guide/concepts/model-providers/ollama/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>aws</category>
      <category>ai</category>
      <category>beginners</category>
    </item>
    <item>
      <title>It Remembers Across Restarts — and Forgets Mid-Conversation</title>
      <dc:creator>Pin Xiong</dc:creator>
      <pubDate>Sat, 07 Nov 2026 01:50:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/it-remembers-across-restarts-and-forgets-mid-conversation-1ehh</link>
      <guid>https://dev.to/aws-builders/it-remembers-across-restarts-and-forgets-mid-conversation-1ehh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Verified on 2026-11-07 against &lt;code&gt;strands-agents&lt;/code&gt; 1.63.0, Python 3.11 and Bedrock in &lt;code&gt;us-east-1&lt;/code&gt;. Every number below came out of a run, and the setup steps live in &lt;code&gt;README.md&lt;/code&gt;. The model is &lt;code&gt;amazon.nova-pro-v1:0&lt;/code&gt; on Amazon Bedrock.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An agent can remember your name across a restart and still lose it seven turns later in the same conversation. Those are two separate failures, and fixing one does nothing for the other.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/posts/how-the-strands-agent-loop-works/" rel="noopener noreferrer"&gt;Article 01&lt;/a&gt; established that an agent's state is one list of messages and that the conversation is the only channel into the model. &lt;a href="https://xiongpin.dev/posts/execution-limits/" rel="noopener noreferrer"&gt;Article 03&lt;/a&gt; bounded a single call. This post follows that list through a restart, a window and a summary, and measures where each one fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  It still knows you after a restart
&lt;/h2&gt;

&lt;p&gt;The simplest memory test runs as two separate OS processes.&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;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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileSessionManager&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.nova-pro-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_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;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileSessionManager&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restart-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./sessions_tmp&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="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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My name is Pin and my favorite number is 42. Just acknowledge, one short sentence.&lt;/span&gt;&lt;span class="sh"&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 first process answers "Hello Pin, it's great to know that 42 is your favorite number!" and exits. Nothing stays in memory. The second process shares nothing with the first except that directory, and it repeats the same imports and the same three setup statements with the same &lt;code&gt;session_id&lt;/code&gt;. The repository keeps both phases in one file and branches on &lt;code&gt;sys.argv[1]&lt;/code&gt;, which is why the figure runs the script twice. If you copy the block above into a second file, keep all four statements; the call alone raises &lt;code&gt;NameError&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is my name and my favorite number?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It answers "Your name is Pin and your favorite number is 42." and then adds a caveat about not storing personal information. No variable carried the fact across the process boundary. A file did.&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%2F3z353stoa6g8awtd2gex.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%2F3z353stoa6g8awtd2gex.png" alt="Two processes in one terminal log: the first plants a name, the second starts from the same session id and recalls it" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cost is visible on disk. After the first run the storage directory holds four files: a &lt;code&gt;session.json&lt;/code&gt; of 165 bytes, an &lt;code&gt;agent.json&lt;/code&gt; of 447 bytes, and one JSON file per message, &lt;code&gt;message_0.json&lt;/code&gt; at 390 bytes and &lt;code&gt;message_1.json&lt;/code&gt; at 594 bytes. After the second run there are six. The message count and the file count move together, one file per message, which is the layout the class docstring draws at &lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/session/file_session_manager.py" rel="noopener noreferrer"&gt;&lt;code&gt;file_session_manager.py:31&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That layout makes a session cheap to append to, and it makes the transcript readable without the SDK. When an agent misbehaves at 3 a.m., &lt;code&gt;cat agents/agent_default/messages/message_17.json&lt;/code&gt; answers more questions than a debugger. The snippets in this post use &lt;code&gt;./sessions_tmp&lt;/code&gt; for readability; the scripts derive the directory from their own location, so on disk it is &lt;code&gt;src/sessions_tmp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things living in agent.json
&lt;/h2&gt;

&lt;p&gt;The 447-byte file from the restart run holds an empty state, because that script never sets any. Open the file a state test writes instead, 528 bytes including two timestamps, and the word memory starts to come apart.&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;"agent_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;"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;"state"&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;"visits"&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;"user"&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;"Pin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro"&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;"conversation_manager_state"&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;"SlidingWindowConversationManager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"removed_message_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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;"model_call_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"_internal_state"&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;"interrupt_state"&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;"interrupts"&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;"context"&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;"activated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;"model_state"&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;That file arrived after two &lt;code&gt;agent.state.set&lt;/code&gt; calls and one turn, and it holds three kinds of memory side by side. The &lt;code&gt;state&lt;/code&gt; block is your key-value store, written by application code. The &lt;code&gt;conversation_manager_state&lt;/code&gt; block is bookkeeping owned by the window manager, including a counter of messages it has already removed. The &lt;code&gt;_internal_state&lt;/code&gt; block belongs to pause and resume handling; for a plain chat agent it stays empty, and it appears here only because it shares the same restore path. All three travel together, because restoring the transcript without the window counters would make the agent re-trim history it had already trimmed.&lt;/p&gt;

&lt;p&gt;These three blocks are not the three lanes drawn later. The lanes are the whole session on disk, the window in memory and the state store; inside &lt;code&gt;agent.json&lt;/code&gt;, the state store is one block among three.&lt;/p&gt;

&lt;p&gt;From here on, state means the &lt;code&gt;agent.state&lt;/code&gt; key-value store, not the message list. The SDK uses the same word for both objects, and keeping them apart is half of this article.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set&lt;/code&gt; and &lt;code&gt;get&lt;/code&gt; take a string key, and values must survive a round trip through JSON. The check happens at write time, which turns a corrupt-file bug into a local error, and the message names the permitted types:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ValueError: Value is not JSON serializable: object. Only JSON-compatible types (str, int, float, bool, list, dict, None) are allowed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The type behind the store is &lt;code&gt;JSONSerializableDict&lt;/code&gt;, so an arbitrary Python object cannot enter it in the first place. A &lt;code&gt;set()&lt;/code&gt; call only mutates memory, though. State reaches disk when the session manager flushes, which happens on the next agent invocation. A process that sets state and exits before calling the model persists nothing.&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%2F5sq7ixezxijyrq1wynz5.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%2F5sq7ixezxijyrq1wynz5.png" alt="A state probe in one terminal: set and read back, a rejected object, then a fresh process reading the same values" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The distinction worth keeping is who reads which store. Messages are for the model. State is for your code. A plan id, a retry budget, the last tool result you care about, none of that needs to occupy the window, and none of it should be reconstructed by asking the model to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  One file instead of six
&lt;/h2&gt;

&lt;p&gt;The message log is one way to put a session on disk, and the SDK ships a second one that answers the same restart test differently.&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;strands.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SnapshotSessionManager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.storage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LocalFileStorage&lt;/span&gt;

&lt;span class="n"&gt;session_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnapshotSessionManager&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snapshot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;LocalFileStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./snapshots_tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;save_latest_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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="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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_handler&lt;/span&gt;&lt;span class="o"&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;p&gt;The same two-process experiment gives the same recall and a different disk: one file, &lt;code&gt;session/snapshot-demo/scopes/agent/default/snapshots/snapshot_latest.json&lt;/code&gt;, at 878 bytes after the first run and 1774 after the second. That &lt;code&gt;session/&lt;/code&gt; prefix comes from the manager wrapping the storage in a namespaced view, so the same prefix appears on S3 as well.&lt;/p&gt;

&lt;p&gt;Where the message log accumulates history as a directory of small files, the snapshot overwrites a single blob that carries transcript, state and manager bookkeeping together. One write is atomic and restores in one read, so the transcript and the removed-message counter can never disagree. The cost is granularity. A crash leaves the most recently saved snapshot, which with &lt;code&gt;save_latest_on="message"&lt;/code&gt; is at most one message behind.&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%2Fd0rxqi9p3pg3asmx8rf2.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%2Fd0rxqi9p3pg3asmx8rf2.png" alt="The same restart run under the snapshot manager: one file before, one file after, 878 bytes then 1774" width="799" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The snapshot docstring at &lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/session/snapshot_session_manager.py" rel="noopener noreferrer"&gt;&lt;code&gt;snapshot_session_manager.py:1&lt;/code&gt;&lt;/a&gt; calls this "the recommended path for new agents" and draws the line to the older managers that persist each message individually. Read that as a direction of travel, not a deprecation notice: the examples I checked still reach for &lt;code&gt;FileSessionManager&lt;/code&gt;, and no session manager is the SDK default, since &lt;code&gt;session_manager&lt;/code&gt; defaults to &lt;code&gt;None&lt;/code&gt; and an agent without one persists nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The window the model actually sees
&lt;/h2&gt;

&lt;p&gt;Now the second question.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SlidingWindowConversationManager&lt;/code&gt; defaults to &lt;code&gt;window_size=40&lt;/code&gt;. By default it runs once at the end of each invocation (&lt;code&gt;per_turn=False&lt;/code&gt;); set &lt;code&gt;per_turn=True&lt;/code&gt; and it runs before every model call instead. The check itself is one comparison in &lt;code&gt;apply_management&lt;/code&gt; at &lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py" rel="noopener noreferrer"&gt;&lt;code&gt;sliding_window_conversation_manager.py:150&lt;/code&gt;&lt;/a&gt;: if &lt;code&gt;len(messages)&lt;/code&gt; is at or below the window, return, otherwise trim. Two limits share the word window here. &lt;code&gt;window_size&lt;/code&gt; counts messages and belongs to the conversation manager. The context window counts tokens and belongs to the model. They are unrelated, and only the first one is in play in this section.&lt;/p&gt;

&lt;p&gt;I set the window to 6 to make the gap observable within seven turns, wiring it the same way as any other manager, imported from &lt;code&gt;strands.agent.conversation_manager&lt;/code&gt; and passed to &lt;code&gt;Agent(...)&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.agent.conversation_manager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SlidingWindowConversationManager&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;conversation_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;SlidingWindowConversationManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;callback_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&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;Turn one plants a codename, five filler turns follow, and turn seven asks for the codename back. Two counters print after every turn: the length of &lt;code&gt;agent.messages&lt;/code&gt;, which is what the model can see, and the number of message files on disk, which is what the session stored.&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%2Fgxvi9r9j6z1njnse01m3.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%2Fgxvi9r9j6z1njnse01m3.png" alt="Seven turns under a window of six: stored messages climb to fourteen while the visible count pins at six, and the codename is gone" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two counters diverge at turn four and never meet again. The arithmetic is worth stating, because it explains the exact turn: every turn appends a user message and an assistant message, so a window of six holds exactly three turns, and turn four is the first one that overflows. The trim then cuts back to &lt;code&gt;window_size&lt;/code&gt; itself, which is why the count pins at six rather than drifting to five or seven. By the end the session holds fourteen message files and the model sees six.&lt;/p&gt;

&lt;p&gt;One nuance the figure cannot show: with &lt;code&gt;FileSessionManager&lt;/code&gt; no message file is deleted. The transcript keeps everything and &lt;code&gt;agent.json&lt;/code&gt; rewrites &lt;code&gt;removed_message_count&lt;/code&gt; to record what the window dropped. A snapshot manager behaves differently, because it saves the trimmed list.&lt;/p&gt;

&lt;p&gt;The model did not say "I do not know your codename." It produced a refusal about private information, as if it had been asked something improper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sorry, I can't respond in a way that might provide private and sensitive information.
This is to maintain the privacy and security of the individuals.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A forgotten fact does not surface as an absence. It surfaces as a confident answer built from whatever is left in the window. If you have ever watched an assistant deny something it was told earlier in the same conversation, this is the mechanism.&lt;/p&gt;

&lt;p&gt;Two memories so far, and they disagree. A session is durable but invisible. A window is visible but ephemeral. Add the third lane and the map is complete.&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%2Fqjzg7l26hii5goexu19a.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%2Fqjzg7l26hii5goexu19a.png" alt="Three stacked lanes joined by downward arrows: session files on disk, the in-memory window, and agent.json state, each labelled with the failure it can cause" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  State does not inject itself
&lt;/h2&gt;

&lt;p&gt;The third lane has a gap of its own. In 1.63.0, &lt;code&gt;agent.state&lt;/code&gt; is written to &lt;code&gt;agent.json&lt;/code&gt; and restored on the next invocation, but nothing in the prompt path ever reads it. &lt;code&gt;FileSessionManager&lt;/code&gt; hands it to &lt;code&gt;SessionAgent.from_agent&lt;/code&gt; (&lt;code&gt;state=agent.state.get()&lt;/code&gt;, &lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/types/session.py" rel="noopener noreferrer"&gt;&lt;code&gt;types/session.py:145&lt;/code&gt;&lt;/a&gt;) and reloads it at &lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/session/repository_session_manager.py" rel="noopener noreferrer"&gt;&lt;code&gt;repository_session_manager.py:262&lt;/code&gt;&lt;/a&gt;; &lt;code&gt;SnapshotSessionManager&lt;/code&gt; goes through &lt;code&gt;take_snapshot&lt;/code&gt; and &lt;code&gt;load_snapshot&lt;/code&gt; instead (&lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/agent.py:2262&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;:2304&lt;/code&gt;). And &lt;code&gt;system_prompt&lt;/code&gt; is a value, typed &lt;code&gt;str | list[SystemContentBlock] | None&lt;/code&gt; (&lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/agent.py:243&lt;/code&gt;&lt;/a&gt;), not a callable. So a fact in state is durable and invisible at the same time: the same failure the window produced, out of a different store.&lt;/p&gt;

&lt;p&gt;If the model has to see it, the injection is yours to write. The smallest working version is a &lt;code&gt;BeforeInvocationEvent&lt;/code&gt; hook (&lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/hooks/events.py" rel="noopener noreferrer"&gt;&lt;code&gt;hooks/events.py:39&lt;/code&gt;&lt;/a&gt;) plus the &lt;code&gt;system_prompt&lt;/code&gt; setter (&lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/agent.py:812&lt;/code&gt;&lt;/a&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;BASE_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 a concise assistant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inject_manifest&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;BeforeInvocationEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&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;state&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="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&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;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;BASE_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;&amp;lt;manifest&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/manifest&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inject_manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BeforeInvocationEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same setup as the sliding-window run, with one addition: the codename goes into both the first user message and &lt;code&gt;agent.state&lt;/code&gt;, and the hook rewrites the system prompt on every invocation. Turn seven now answers with the codename, even though it left the window at turn four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORION_IN_MESSAGES_BEFORE_ASK: False
TURN 7 ask     | msgs= 6 stored=14 | ORION
ORION_IN_SYSTEM_PROMPT: True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook fires before every invocation, not once at construction, so the manifest tracks state as it changes. The channel is not free either: it repeats the serialized state in the prompt on every call, which is a fair price for an invariant and a bad one for a log. Which restates the original rule with the machinery filled in. Messages are for the model, state is for your code, and the bridge between them is a hook you write yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarizing, but only at the cliff
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to summarize the oldest messages instead of dropping them, and my first attempt at demonstrating it failed in an instructive way.&lt;/p&gt;

&lt;p&gt;I ran the same seven turns with &lt;code&gt;summary_ratio=0.3&lt;/code&gt;, &lt;code&gt;preserve_recent_messages=2&lt;/code&gt; and proactive compression armed at 0.001, about 300 tokens on this model. At the end there were 14 messages, no summary anywhere, and the codename still sitting in the raw first message. The manager had never fired.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;THRESHOLD   : 0.001
TURN 7 ask     | msgs=14 | Your codename is ORION, as you previously stated. Is there
                 anything else I can assist you with?
ROLES_IN_WINDOW: ['user', 'assistant', 'user', 'assistant', ...]
ORION_VISIBLE_TO_MODEL: True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is in the code, not the name. This manager has no window size at all, and its &lt;code&gt;apply_management&lt;/code&gt; leaves history untouched. Summarization happens on exactly two triggers: a real context overflow, or the base class's proactive check. Neither fired, because fourteen short messages occupy a rounding error of a 300,000-token context window. The manager was not ignoring the conversation; it was waiting for a cliff that a demo conversation never reaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the cliff reachable
&lt;/h2&gt;

&lt;p&gt;Proactive compression is the knob that makes the cliff reachable. It runs before every model call at &lt;a href="https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/conversation_manager/conversation_manager.py" rel="noopener noreferrer"&gt;&lt;code&gt;conversation_manager.py:132&lt;/code&gt;&lt;/a&gt;, where the check divides projected input tokens by the context window and compares the ratio against a threshold, 0.7 by default and with no floor.&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;strands.agent.conversation_manager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SummarizingConversationManager&lt;/span&gt;

&lt;span class="n"&gt;session_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileSessionManager&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./sessions_tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;conversation_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SummarizingConversationManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;summary_ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;preserve_recent_messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;proactive_compression&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;compression_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.00005&lt;/span&gt;&lt;span class="p"&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="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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;conversation_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;conversation_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callback_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&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;On a 300,000-token window, 0.00005 works out to fifteen tokens, small enough that the conversation crosses it immediately. The same seven turns then behave differently.&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%2Fmhnrb6pz1dywvsioyjfw.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%2Fmhnrb6pz1dywvsioyjfw.png" alt="Seven turns under the summarizer: the count rises by two per turn, then by one, settling at nine, with the codename reported visible" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The count rose by two per turn until the compression engaged, then by one. Each firing selects &lt;code&gt;max(1, int(len(messages) * summary_ratio))&lt;/code&gt; messages, capped by &lt;code&gt;preserve_recent_messages&lt;/code&gt;, and replaces them with a single summary, so two or three old messages collapse into one and the net gain per turn drops to a single entry. Turn seven is the exception: nine stays nine, because the run ends with &lt;code&gt;removed_message_count: 6&lt;/code&gt; and one summary the manager re-injects, and 14 − 6 + 1 = 9. Nine is where this transcript settled, not a fixed ceiling.&lt;/p&gt;

&lt;p&gt;What did not change is the role. The window still opens with two &lt;code&gt;user&lt;/code&gt; entries, because the summary itself is injected as a user message. What replaced the original turn is its content, not its role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Conversation Summary
* Topic 1: Request for conversation summary
  * The user, identified by the codename ORION, requested a summary of the conversation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The codename survived, and it survived as a line inside a model-written summary, which is a weaker guarantee than a file on disk. A summary is lossy and the summarizer is the same model whose memory you are trying to protect. This run also logged a warning worth reading twice: "Cannot summarize: only the previous summary is old enough".&lt;/p&gt;

&lt;p&gt;With a short window and &lt;code&gt;summary_ratio=0.3&lt;/code&gt;, the selection rounds down to one message, and that one message is the summary it wrote last time. The manager declines, logs, and continues rather than raising, which is right for a context-pressure path and easy to miss in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;The first surprise is the shape of forgetting. I expected a missing codename to produce "I do not know". Instead the model built a plausible refusal out of the six messages it could still see. Memory failure in an agent looks like hallucination, and that makes it much harder to catch in a log review than a raised exception.&lt;/p&gt;

&lt;p&gt;The second is that &lt;code&gt;SummarizingConversationManager&lt;/code&gt; does not summarize on message count. The name suggests a window; the implementation waits for context pressure. Anyone who configures it expecting sliding-window behavior, and never crosses the threshold, gets an agent that grows without bound while the configuration looks correct.&lt;/p&gt;

&lt;p&gt;The third is how small the distance is between working and not. The mechanism never changed between the two runs above. Only the ratio did: 0.001 was invisible, 0.00005 was decisive, on the same model and the same seven turns.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use this
&lt;/h2&gt;

&lt;p&gt;If every invocation is a single turn with no follow-up, skip sessions entirely. A session manager writes files you will never read and restores state you will never reuse, and the cost is paid on every call.&lt;/p&gt;

&lt;p&gt;Do not use &lt;code&gt;agent.state&lt;/code&gt; as a database. It is a small JSON dict that travels with the agent snapshot, which makes it right for a plan id or a retry counter and wrong for anything large, queryable or shared across agents.&lt;/p&gt;

&lt;p&gt;And do not treat a summary as a record. If a fact has to survive compression, put it in state and inject it as above, or write it into the system prompt directly, not in the hope that the summarizer keeps it. The codename survived here because it was the most salient thing in seven turns. A detail mentioned once inside a long tool result will not be so lucky.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pinxiong/strands-in-action.git
&lt;span class="nb"&gt;cd &lt;/span&gt;strands-in-action/usecases/04-sessions-state-memory
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.11
&lt;span class="nv"&gt;VIRTUAL_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.venv uv pip &lt;span class="nb"&gt;install &lt;/span&gt;strands-agents&lt;span class="o"&gt;==&lt;/span&gt;1.63.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with AWS credentials that can call &lt;code&gt;amazon.nova-pro-v1:0&lt;/code&gt; in &lt;code&gt;us-east-1&lt;/code&gt;. Delete &lt;code&gt;src/sessions_tmp/&lt;/code&gt; and &lt;code&gt;src/snapshots_tmp/&lt;/code&gt; before every rerun: the session ids are hard-coded, so a second &lt;code&gt;first&lt;/code&gt; appends to the previous transcript and the file counts above stop matching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; src/sessions_tmp src/snapshots_tmp
python src/session_restart.py first    &lt;span class="c"&gt;# plant a name, list the files&lt;/span&gt;
python src/session_restart.py second   &lt;span class="c"&gt;# fresh process, same session id&lt;/span&gt;
python src/agent_state.py first        &lt;span class="c"&gt;# state set/get, JSON rejection&lt;/span&gt;
python src/agent_state.py second       &lt;span class="c"&gt;# state after a restart&lt;/span&gt;
python src/sliding_window.py           &lt;span class="c"&gt;# fourteen stored, six visible&lt;/span&gt;
python src/state_manifest.py           &lt;span class="c"&gt;# same window, codename returns via the manifest&lt;/span&gt;
python src/summarizing_window.py       &lt;span class="c"&gt;# summary keeps the codename&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; src/sessions_tmp/session_summary-demo
&lt;span class="nv"&gt;COMPRESSION_THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.001 python src/summarizing_window.py   &lt;span class="c"&gt;# nothing happens&lt;/span&gt;
python src/snapshot_session.py first   &lt;span class="c"&gt;# one snapshot blob&lt;/span&gt;
python src/snapshot_session.py second  &lt;span class="c"&gt;# recall from that blob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two summarizing runs share &lt;code&gt;session_id="summary-demo"&lt;/code&gt;, which is why the &lt;code&gt;rm -rf&lt;/code&gt; sits between them. Without it the second run restores the nine messages the first one left behind, and the counts above will not repeat.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Sessions are files, windows are lists in memory. The first survives a restart, the second survives nothing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent.json&lt;/code&gt; carries three kinds of memory at once: application state, window bookkeeping and interrupt state, restored together.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent.state&lt;/code&gt; refuses a non-JSON value the moment you set it, and reaches disk only on the next invocation.&lt;/li&gt;
&lt;li&gt;Stored is not surfaced: &lt;code&gt;agent.state&lt;/code&gt; reaches the model only if you inject it, and a &lt;code&gt;BeforeInvocationEvent&lt;/code&gt; hook rewriting the system prompt is the smallest bridge.&lt;/li&gt;
&lt;li&gt;A window of six holds three turns, so turn four is where visible and stored history part ways.&lt;/li&gt;
&lt;li&gt;A forgotten fact comes back as a confident refusal, so overflow looks like hallucination rather than amnesia.&lt;/li&gt;
&lt;li&gt;Summarizing is triggered by context pressure, not message count; below the threshold it never runs at all.&lt;/li&gt;
&lt;li&gt;The threshold is a ratio with no floor, which is what makes compression testable in a demo.&lt;/li&gt;
&lt;li&gt;A summary message keeps the &lt;code&gt;user&lt;/code&gt; role and loses detail, so it is a weaker record than state.&lt;/li&gt;
&lt;li&gt;One snapshot file trades a readable audit trail for consistent restores, at most one message behind when saving on every message.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>strands</category>
      <category>aws</category>
    </item>
    <item>
      <title>Six Serving Engines on One EC2 G6: Gemma 4 E2B on an NVIDIA L4</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 06 Nov 2026 18:41:59 +0000</pubDate>
      <link>https://dev.to/aws-builders/six-serving-engines-on-one-ec2-g6-gemma-4-e2b-on-an-nvidia-l4-3ff3</link>
      <guid>https://dev.to/aws-builders/six-serving-engines-on-one-ec2-g6-gemma-4-e2b-on-an-nvidia-l4-3ff3</guid>
      <description>&lt;p&gt;This article provides a step by step comparison of six inference serving engines on one Amazon EC2 G6 instance: llama.cpp, vLLM, SGLang, mistral.rs, a plain PyTorch engine and Google's LiteRT-LM, each serving Gemma 4 E2B on Google's QAT 4-bit weights from one NVIDIA L4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The question is which engine to put on a G6 when one small model has the whole GPU. Each engine gets the same weights, the same prompts, the same 1-to-64-request load sweep and the same 1,319 GSM8K and 400 BFCL questions, and each is measured with and without Google's E2B drafter wherever the engine supports one. Every engine ran on one instance, one at a time, with the card to itself.&lt;/p&gt;




&lt;h4&gt;
  
  
  The Results in Brief
&lt;/h4&gt;

&lt;p&gt;Answer quality barely moves between engines. GSM8K lands between 0.8954 and 0.9014 on the five CUDA engines with the drafter on or off, and every paired difference against llama.cpp is within the run-to-run range. LiteRT-LM scores level on GSM8K and lower on BFCL.&lt;/p&gt;

&lt;p&gt;For one request, five engines are within 8% of each other without a drafter (134.7 to 144.6 tokens per second end to end) and LiteRT-LM runs at 0.62 to 0.73x llama.cpp. The drafter decides the order: it gives vLLM 1.96x, llama.cpp 1.53x, SGLang 1.39x and LiteRT-LM 1.15x, and it slows mistral.rs to 0.95x.&lt;/p&gt;

&lt;p&gt;Under load the batching engines separate. At 64 requests of 128 tokens in and 512 out, vLLM reaches 3,716 tokens per second without a drafter and 4,516 with it, SGLang 2,967, mistral.rs 1,825 and llama.cpp 1,056. PyTorch and LiteRT-LM serve one request at a time.&lt;/p&gt;

&lt;p&gt;With each engine at its best setting, vLLM is the fastest at every level from 1 to 64 requests, on both shapes, and its drafter never loses. At the g6.4xlarge on-demand price that is $0.08 per million output tokens at full load.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Changes and What Stays
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Weights&lt;/th&gt;
&lt;th&gt;Installed with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;&lt;code&gt;7182ef71e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;exact Q4_0 GGUF&lt;/td&gt;
&lt;td&gt;CMake, CUDA 13.1, sm_89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;0.33.0&lt;/td&gt;
&lt;td&gt;W4A16, int4 tables&lt;/td&gt;
&lt;td&gt;pip, its own &lt;code&gt;torch 2.14.0+cu130&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang&lt;/td&gt;
&lt;td&gt;0.5.23&lt;/td&gt;
&lt;td&gt;W4A16, int4 tables&lt;/td&gt;
&lt;td&gt;pip, cp313 wheel on Python 3.14, two patches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral.rs&lt;/td&gt;
&lt;td&gt;v0.9.5&lt;/td&gt;
&lt;td&gt;exact Q4_0 GGUF&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cargo install&lt;/code&gt;, CUDA 13.1, sm_89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;this repo's int4 engine&lt;/td&gt;
&lt;td&gt;W4A16&lt;/td&gt;
&lt;td&gt;the DLAMI's torch 2.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteRT-LM&lt;/td&gt;
&lt;td&gt;0.19.0&lt;/td&gt;
&lt;td&gt;exact Q4_0, and Google's int2/4/8 file&lt;/td&gt;
&lt;td&gt;pip, GPU through WebGPU on Vulkan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same for all six: one g6.4xlarge on-demand in us-east-1d (16 vCPUs, 64 GiB, one NVIDIA L4 with 23,034 MiB, driver 595.112.07), the PyTorch 2.13 / Ubuntu 26.04 Deep Learning AMI, Gemma 4 E2B on Google's QAT Q4_0 grid in every container, greedy decoding, and the system Python 3.14 with no virtual environment. The exact Q4_0 GGUF and the W4A16 checkpoint carry the same 4-bit values: &lt;code&gt;xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf&lt;/code&gt; for llama.cpp and mistral.rs, &lt;code&gt;xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4&lt;/code&gt; for vLLM and SGLang.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have…
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with G-family vCPU quota in us-east-1 and an instance profile with SSM access&lt;/li&gt;
&lt;li&gt;An S3 bucket for moving files to and from the instance&lt;/li&gt;
&lt;li&gt;The system &lt;code&gt;python3&lt;/code&gt; with &lt;code&gt;boto3&lt;/code&gt;, and the MCP server in &lt;code&gt;gpu-pytorch-g6-2b-q4w4a16/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A clone: &lt;code&gt;git clone https://github.com/xbill9/gemma4-dev&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Find a G6 With Capacity
&lt;/h4&gt;

&lt;p&gt;The rigs name a g6.2xlarge (8 vCPUs, one L4). On 2026-11-05 it had no spot or on-demand capacity in any us-east-1 zone, and neither did g6.4xlarge spot. The MCP tool &lt;code&gt;create_g6_instance&lt;/code&gt; walks every subnet, spot first and then on-demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subnet-0c2872fe4182b9ec1 spot=True: ❌ AWS InsufficientInstanceCapacity: There is no Spot capacity available that matches your request.
subnet-09e0f13c0a5b43092 spot=True: ❌ AWS InsufficientInstanceCapacity: There is no Spot capacity available that matches your request.
subnet-0c2872fe4182b9ec1 spot=False: ❌ AWS InsufficientInstanceCapacity: We currently do not have sufficient g6.4xlarge capacity in the Availability Zone you requested (us-east-
subnet-0d2b2038965e8d0c0 spot=False: ✅ Launching `i-040ed957b67c2d514` (g6.4xlarge, on-demand, 1x L4) in `us-east-1`.
AMI: `ami-0f5139ed9ed52b805`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sizes carry the same L4. The g6.4xlarge doubles the vCPUs and memory, which only llama.cpp's prompt threads and the build steps use.&lt;/p&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: all access is SSM and S3.&lt;/strong&gt; The instance has no SSH key and its role has no S3 access. Commands go through SSM Run Command, which returns at most 24,000 characters, so files larger than that move through presigned S3 URLs generated on the workstation.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — One Rig Directory per Engine
&lt;/h4&gt;

&lt;p&gt;Each engine has its own directory with a &lt;code&gt;tpu.env&lt;/code&gt; that decides every serving flag, a Makefile run on the instance, and its measurements under &lt;code&gt;benchmarks/runs/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpu-llamacpp-g6-2b-q4_0/
gpu-vllm-g6-2b-q4w4a16emb4/
gpu-sglang-g6-2b-q4w4a16emb4/
gpu-mistralrs-g6-2b-q4_0/
gpu-pytorch-g6-2b-q4w4a16/
gpu-litertlm-g6-2b-int248/
gpu-litertlm-g6-2b-q4w4a16emb4/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gpu-vllm-g6-2b-q4w4a16emb4/flight/setup.sh&lt;/code&gt; installs every engine in order and logs each step, so a failed step reruns alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GEMMA4_DEV=/opt/gemma4-dev MODEL_S3_URI=s3://... bash setup.sh llamacpp vllm sglang mistralrs litert
18:48:44 START llamacpp
18:56:56 END llamacpp
18:56:56 START vllm
18:59:18 END vllm
18:59:18 START sglang
18:59:57 END sglang
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 3 — What the Deep Learning AMI Needs
&lt;/h4&gt;

&lt;p&gt;The AMI ships the NVIDIA driver and a PyTorch 2.13 environment, and no CUDA toolkit. Four things differ from a desktop install:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vLLM brings its own torch.&lt;/strong&gt; The system Python has no torch, so &lt;code&gt;pip install vllm==0.33.0&lt;/code&gt; pulls its pinned wheel from PyPI, built for CUDA 13.0, and it runs on driver 595.112.07:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;torch 2.14.0+cu130 cuda 13.0 NVIDIA L4 (8, 9)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp and mistral.rs build against CUDA 13.1&lt;/strong&gt; from NVIDIA's apt repository, for sm_89. mistral.rs also links cuRAND, which is a separate package: &lt;code&gt;libcurand-dev-13-1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteRT-LM finds the L4 through Vulkan.&lt;/strong&gt; The AMI lists the L4 as a Vulkan device, and LiteRT-LM's WebGPU backend serves on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pip puts scripts in &lt;code&gt;~/.local/bin&lt;/code&gt;.&lt;/strong&gt; SSM runs commands as root with &lt;code&gt;HOME=/root&lt;/code&gt;, and that directory is off its &lt;code&gt;PATH&lt;/code&gt;, so &lt;code&gt;vllm&lt;/code&gt;, &lt;code&gt;litert-lm&lt;/code&gt; and SGLang's &lt;code&gt;ninja&lt;/code&gt; are found only once it is added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: SGLang 0.5.23 needs two patches for this checkpoint.&lt;/strong&gt; Its Gemma 4 attention shim runs only for multimodal configs, and it leaves the int4 &lt;code&gt;embed_tokens&lt;/code&gt; and per-layer table as uninitialized bf16. The rig's &lt;code&gt;patches/&lt;/code&gt; keep both tables packed on the card.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Size Each Engine for the Whole Card
&lt;/h4&gt;

&lt;p&gt;The card serves one engine at a time and nothing else, so each engine gets the most the L4 will hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vLLM&lt;/strong&gt; reserves 85% of the card (&lt;code&gt;--gpu-memory-utilization 0.85&lt;/code&gt;), 64 running requests, 16,384 tokens each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SGLang&lt;/strong&gt; reserves 80% (&lt;code&gt;--mem-fraction-static 0.80&lt;/code&gt;), 64 running requests, decode CUDA graphs up to 64.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp&lt;/strong&gt; runs 64 slots over a 65,536-token cache for the load sweep, and one slot of 131,072 for accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mistral.rs&lt;/strong&gt; runs 32 requests over a fixed 16,384-token cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: mistral.rs plans activation memory per running request.&lt;/strong&gt; It sets aside about 395 MiB for each, so 64 requests do not load on the L4's 23 GB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Primary device cuda[0] needs 28292 MiB (2976 MiB of fixed model components, 25316 MiB of activations, 0 MiB of post-load reservations), but only 21192 MiB is usable under the memory goal.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 32 requests a 65,536-token cache leaves too little room; &lt;code&gt;--max-seqs 32 --kv-cache 16384tokens --max-num-batched-tokens 4096&lt;/code&gt; serves in 5,286 MiB.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — One Request at a Time
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;flight/engines.py&lt;/code&gt; sends six chat prompts, 256 tokens each, three times per engine, and takes the geometric mean of each prompt's median, end to end. Then 4 requests at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arm              e2e tok/s vs llama  decode TTFT ms 4 at once  x one    memory ready s repeat
llama                135.7    1.000   136.9      29     437.6   3.23  2930 MiB     6.1   6/6
llama-d2             207.7    1.531   211.4      30     373.2   1.80  3004 MiB     4.1   6/6
mistralrs            144.6    1.066   144.4      11     500.0   3.46  4664 MiB    24.1   6/6
pytorch              134.7    0.993       -       -     134.5   1.00  3248 MiB   236.2   6/6
litert-int248         98.4    0.725   125.2     512     100.5   1.02  1489 MiB     9.4   6/6
vllm                 140.4    1.035   140.7      16     537.2   3.83 18228 MiB   167.3   6/6
vllm-d4              275.3    2.030   281.2      19     711.3   2.58 18918 MiB   132.5   6/6
sglang               136.5    1.006   139.7      50     475.4   3.48 18345 MiB   307.2   6/6
sglang-d4            189.6    1.398   198.1      55     508.3   2.68 15191 MiB    61.1   6/6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;No drafter&lt;/th&gt;
&lt;th&gt;Best drafter&lt;/th&gt;
&lt;th&gt;Drafter gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;140.4&lt;/td&gt;
&lt;td&gt;🥇 275.3 (4 tokens)&lt;/td&gt;
&lt;td&gt;1.96x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;135.7&lt;/td&gt;
&lt;td&gt;207.7 (2 tokens)&lt;/td&gt;
&lt;td&gt;1.53x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang&lt;/td&gt;
&lt;td&gt;136.5&lt;/td&gt;
&lt;td&gt;189.6 (4 tokens)&lt;/td&gt;
&lt;td&gt;1.39x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral.rs&lt;/td&gt;
&lt;td&gt;144.6&lt;/td&gt;
&lt;td&gt;137.6 (1 token)&lt;/td&gt;
&lt;td&gt;0.95x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;134.7&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteRT-LM&lt;/td&gt;
&lt;td&gt;98.4&lt;/td&gt;
&lt;td&gt;112.8 (its own)&lt;/td&gt;
&lt;td&gt;1.15x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;LiteRT-LM's exact Q4_0 build runs at 84.4 tokens per second, and LiteRT-LM takes about half a second to its first token against 11 to 55 ms for the others. Every engine repeats its own greedy text on all six prompts.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — 1 to 64 Requests
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;sweep.py&lt;/code&gt; runs two shapes, 512 tokens in / 128 out and 128 in / 512 out, at 1 to 64 requests at once, five repeats, a unique prompt per request so no prefix cache can hit, output timed from the stream. &lt;code&gt;flight/suite.py&lt;/code&gt; runs every sweep and every accuracy job one server at a time, and marks each finished job so a rerun skips it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 suite.py /opt/flight-out/2026-11-06-suite-g6 sweep:vllm,sweep:vllm-d1,sweep:vllm-d4,sweep:sglang,...
2026-11-06 04:06:20 START sweep:vllm ready_s=39.1 gpu=52, 2040 MHz, 19296 MiB
2026-11-06 04:07:33   sweep:vllm in512-out128 rc=0
2026-11-06 04:10:07   sweep:vllm in128-out512 rc=0
2026-11-06 04:10:08 END sweep:vllm ok=True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aggregate output tokens per second without a drafter, 128 in / 512 out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;SGLang&lt;/th&gt;
&lt;th&gt;mistral.rs&lt;/th&gt;
&lt;th&gt;llama.cpp&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;138.8&lt;/td&gt;
&lt;td&gt;134.6&lt;/td&gt;
&lt;td&gt;133.9&lt;/td&gt;
&lt;td&gt;133.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;991.0&lt;/td&gt;
&lt;td&gt;967.4&lt;/td&gt;
&lt;td&gt;626.5&lt;/td&gt;
&lt;td&gt;597.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;🥇 3,715.7&lt;/td&gt;
&lt;td&gt;2,967.1&lt;/td&gt;
&lt;td&gt;1,825.2&lt;/td&gt;
&lt;td&gt;1,056.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;512 in / 128 out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;SGLang&lt;/th&gt;
&lt;th&gt;mistral.rs&lt;/th&gt;
&lt;th&gt;llama.cpp&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;132.8&lt;/td&gt;
&lt;td&gt;129.6&lt;/td&gt;
&lt;td&gt;133.1&lt;/td&gt;
&lt;td&gt;125.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;738.3&lt;/td&gt;
&lt;td&gt;708.3&lt;/td&gt;
&lt;td&gt;591.2&lt;/td&gt;
&lt;td&gt;428.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;🥇 1,593.7&lt;/td&gt;
&lt;td&gt;1,373.0&lt;/td&gt;
&lt;td&gt;1,132.0&lt;/td&gt;
&lt;td&gt;630.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 126 levels completed. vLLM's output at 64 requests is 26.8x its one-request rate on long replies. SGLang reaches 0.80 to 0.86x vLLM at 64, mistral.rs 0.49 to 0.71x and llama.cpp 0.28 to 0.40x.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Each Engine at Its Best
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;best_per_level.py&lt;/code&gt; picks each engine's fastest measured setting at each level, 128 in / 512 out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;SGLang&lt;/th&gt;
&lt;th&gt;llama.cpp&lt;/th&gt;
&lt;th&gt;mistral.rs&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;🥇 453 (4 drafted)&lt;/td&gt;
&lt;td&gt;283 (4 drafted)&lt;/td&gt;
&lt;td&gt;271 (2 drafted)&lt;/td&gt;
&lt;td&gt;134 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;🥇 856 (4 drafted)&lt;/td&gt;
&lt;td&gt;552 (4 drafted)&lt;/td&gt;
&lt;td&gt;430 (2 drafted)&lt;/td&gt;
&lt;td&gt;254 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;🥇 1,483 (4 drafted)&lt;/td&gt;
&lt;td&gt;875 (4 drafted)&lt;/td&gt;
&lt;td&gt;596 (2 drafted)&lt;/td&gt;
&lt;td&gt;468 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;🥇 2,399 (4 drafted)&lt;/td&gt;
&lt;td&gt;1,223 (4 drafted)&lt;/td&gt;
&lt;td&gt;791 (2 drafted)&lt;/td&gt;
&lt;td&gt;627 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;🥇 2,823 (4 drafted)&lt;/td&gt;
&lt;td&gt;1,673 (none)&lt;/td&gt;
&lt;td&gt;1,021 (2 drafted)&lt;/td&gt;
&lt;td&gt;1,339 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;🥇 3,665 (1 drafted)&lt;/td&gt;
&lt;td&gt;2,257 (none)&lt;/td&gt;
&lt;td&gt;1,132 (2 drafted)&lt;/td&gt;
&lt;td&gt;1,843 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;🥇 4,516 (4 drafted)&lt;/td&gt;
&lt;td&gt;2,967 (none)&lt;/td&gt;
&lt;td&gt;1,170 (2 drafted)&lt;/td&gt;
&lt;td&gt;1,825 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;512 in / 128 out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;SGLang&lt;/th&gt;
&lt;th&gt;llama.cpp&lt;/th&gt;
&lt;th&gt;mistral.rs&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;🥇 345 (4 drafted)&lt;/td&gt;
&lt;td&gt;214 (4 drafted)&lt;/td&gt;
&lt;td&gt;224 (2 drafted)&lt;/td&gt;
&lt;td&gt;133 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;🥇 1,217 (4 drafted)&lt;/td&gt;
&lt;td&gt;711 (4 drafted)&lt;/td&gt;
&lt;td&gt;494 (2 drafted)&lt;/td&gt;
&lt;td&gt;591 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;🥇 1,389 (1 drafted)&lt;/td&gt;
&lt;td&gt;1,026 (none)&lt;/td&gt;
&lt;td&gt;572 (2 drafted)&lt;/td&gt;
&lt;td&gt;1,121 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;🥇 1,710 (1 drafted)&lt;/td&gt;
&lt;td&gt;1,373 (none)&lt;/td&gt;
&lt;td&gt;631 (none)&lt;/td&gt;
&lt;td&gt;1,132 (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vLLM's drafter pays at every level: with 4 drafted tokens 2.60x / 3.26x no drafter for one request, still 1.02x / 1.22x at 64. SGLang's drafter pays up to 4 to 8 requests and loses from 16 (0.73x / 0.69x at 64). llama.cpp's 2-token drafter pays at every level on long replies (1.11x at 64) and up to 16 requests on long prompts.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Answer Quality
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;gen_eval.py&lt;/code&gt; asks each engine all 1,319 GSM8K test questions (2,048-token limit) and the 400 BFCL simple function-calling questions (512), one request at a time, greedy, thinking off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;GSM8K&lt;/th&gt;
&lt;th&gt;BFCL&lt;/th&gt;
&lt;th&gt;GSM8K with drafter&lt;/th&gt;
&lt;th&gt;BFCL with drafter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;0.8984&lt;/td&gt;
&lt;td&gt;0.9250&lt;/td&gt;
&lt;td&gt;0.8976&lt;/td&gt;
&lt;td&gt;0.9200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;0.8969&lt;/td&gt;
&lt;td&gt;0.9225&lt;/td&gt;
&lt;td&gt;0.8992&lt;/td&gt;
&lt;td&gt;0.9225&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang&lt;/td&gt;
&lt;td&gt;0.8954&lt;/td&gt;
&lt;td&gt;0.9150&lt;/td&gt;
&lt;td&gt;0.8992&lt;/td&gt;
&lt;td&gt;0.9150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral.rs&lt;/td&gt;
&lt;td&gt;0.9014&lt;/td&gt;
&lt;td&gt;0.9200&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;0.8961&lt;/td&gt;
&lt;td&gt;0.9225&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteRT-LM, exact Q4_0&lt;/td&gt;
&lt;td&gt;0.8999&lt;/td&gt;
&lt;td&gt;0.8600&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteRT-LM, Google's file&lt;/td&gt;
&lt;td&gt;0.8908&lt;/td&gt;
&lt;td&gt;0.8250&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Paired question by question, every engine is level with llama.cpp on GSM8K, and the drafter changes no score. On BFCL, SGLang is 4 questions behind llama.cpp, a gap bigger than the run-to-run range, and LiteRT-LM is 26 and 40 behind.&lt;/p&gt;

&lt;p&gt;The drafter cuts the time to finish GSM8K:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;No drafter&lt;/th&gt;
&lt;th&gt;With drafter&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;3,918 s&lt;/td&gt;
&lt;td&gt;🥇 1,270 s&lt;/td&gt;
&lt;td&gt;3.08x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang&lt;/td&gt;
&lt;td&gt;4,035 s&lt;/td&gt;
&lt;td&gt;1,956 s&lt;/td&gt;
&lt;td&gt;2.06x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;3,971 s&lt;/td&gt;
&lt;td&gt;2,007 s&lt;/td&gt;
&lt;td&gt;1.98x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral.rs&lt;/td&gt;
&lt;td&gt;3,918 s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;3,991 s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteRT-LM&lt;/td&gt;
&lt;td&gt;5,018 s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: send thinking off explicitly to SGLang and mistral.rs.&lt;/strong&gt; &lt;code&gt;"chat_template_kwargs": {"enable_thinking": false}&lt;/code&gt; keeps replies short on every engine; mistral.rs thinks by default on Gemma 4.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 9 — Cost
&lt;/h4&gt;

&lt;p&gt;At the g6.4xlarge on-demand price, $1.3232 an hour in us-east-1 from the AWS Price List API, the measured throughput works out to (arithmetic, from &lt;code&gt;cost.py&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;128 in / 512 out&lt;/th&gt;
&lt;th&gt;512 in / 128 out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM + drafter, 1 request&lt;/td&gt;
&lt;td&gt;$0.81 per M tokens&lt;/td&gt;
&lt;td&gt;$1.06 per M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM + drafter, 64 requests&lt;/td&gt;
&lt;td&gt;🥇 $0.08&lt;/td&gt;
&lt;td&gt;🥇 $0.21 (1 drafted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang, 64 requests&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral.rs, 64 requests&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;$0.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp + drafter, 64 requests&lt;/td&gt;
&lt;td&gt;$0.31&lt;/td&gt;
&lt;td&gt;$0.59&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole sweep and accuracy suite ran 12.2 hours, $16.14 on demand. A g6.2xlarge carries the same L4 at $0.9776 an hour; it was not measured here.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 10 — Tear Down
&lt;/h4&gt;

&lt;p&gt;Results come off the instance as one tarball through a presigned S3 PUT, and then the instance is terminated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -c "import boto3; print(boto3.client('ec2', region_name='us-east-1').terminate_instances(InstanceIds=['i-040ed957b67c2d514'])['TerminatingInstances'][0]['CurrentState']['Name'])"
shutting-down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&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;vLLM&lt;/th&gt;
&lt;th&gt;SGLang&lt;/th&gt;
&lt;th&gt;llama.cpp&lt;/th&gt;
&lt;th&gt;mistral.rs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 request, best&lt;/td&gt;
&lt;td&gt;🥇 275.3&lt;/td&gt;
&lt;td&gt;189.6&lt;/td&gt;
&lt;td&gt;207.7&lt;/td&gt;
&lt;td&gt;144.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 requests, 128/512, best&lt;/td&gt;
&lt;td&gt;🥇 4,516&lt;/td&gt;
&lt;td&gt;2,967&lt;/td&gt;
&lt;td&gt;1,170&lt;/td&gt;
&lt;td&gt;1,825&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drafter under load&lt;/td&gt;
&lt;td&gt;🟢 pays at every level&lt;/td&gt;
&lt;td&gt;❌ 0.69x at 64&lt;/td&gt;
&lt;td&gt;🟢 to 16–64&lt;/td&gt;
&lt;td&gt;❌ slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSM8K level&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BFCL level&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;⚠️ 4 behind&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs as installed&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;❌ two patches&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;⚠️ needs cuRAND, fixed cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PyTorch and LiteRT-LM serve one request at a time:&lt;/p&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;PyTorch&lt;/th&gt;
&lt;th&gt;LiteRT-LM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 request, best&lt;/td&gt;
&lt;td&gt;134.7&lt;/td&gt;
&lt;td&gt;112.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSM8K level&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BFCL level&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;❌ 26–40 behind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs as installed&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;⚠️ Vulkan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;On a G6 serving one small model, run vLLM with Google's W4A16 drafter at 4 tokens: it is the fastest engine for one request (275.3 tokens per second) and at every load to 64 requests, it finishes GSM8K 3.08x sooner than without the drafter at the same score, and at full load it costs $0.08 per million output tokens on demand. SGLang without a drafter reaches 0.66x of that at 64 requests and needs two patches. llama.cpp with its 2-token drafter is the simplest to build and serves one user at 207.7 tokens per second, but tops out near 1,170 tokens per second. mistral.rs, PyTorch and LiteRT-LM suit one user at a time.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to find the inference serving engine to run for Gemma 4 E2B on an Amazon EC2 G6. The key was holding everything else fixed: one instance, the same 4-bit values in every container, the same prompts, load sweep and question sets, and each engine sized for the whole L4. The six-engine results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 All six engines give the same GSM8K answers (0.8908 to 0.9014) with the drafter on or off.&lt;/li&gt;
&lt;li&gt;🟢 vLLM with its drafter is the fastest engine at every level from 1 to 64 requests: 275.3 tokens per second for one request and 4,516 at 64 (128 in / 512 out).&lt;/li&gt;
&lt;li&gt;🟢 vLLM's drafter pays at every load on the L4, and finishes GSM8K 3.08x sooner.&lt;/li&gt;
&lt;li&gt;🟢 At full load vLLM serves a million output tokens for $0.08 on demand.&lt;/li&gt;
&lt;li&gt;⚠️ The drafter's gain depends on the engine: 1.96x vLLM, 1.53x llama.cpp, 1.39x SGLang, 1.15x LiteRT-LM, 0.95x mistral.rs.&lt;/li&gt;
&lt;li&gt;⚠️ g6.2xlarge had no capacity in us-east-1 on the day; g6.4xlarge on-demand carries the same L4.&lt;/li&gt;
&lt;li&gt;❌ mistral.rs loads at most 32 requests on the L4's 23 GB.&lt;/li&gt;
&lt;li&gt;❌ SGLang's drafter cuts output to 0.69 to 0.73x of running without it at 64 requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one g6.4xlarge on-demand in us-east-1d, one NVIDIA L4, the Deep Learning AMI with driver 595.112.07, Gemma 4 E2B on Google's QAT Q4_0 grid, one engine at a time, greedy decoding. One-request speed is six prompts of 256 tokens, three repeats; load is two synthetic shapes at 1 to 64 requests, five repeats each; accuracy is one pass of GSM8K (1,319) and BFCL simple (400). The one-request runs were on 2026-11-05 and the sweeps and accuracy on 2026-11-06, on the same instance; costs are arithmetic from the on-demand price.&lt;/p&gt;

&lt;p&gt;The strategy for comparing inference serving engines on Amazon EC2 G6 was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;gemma4-dev on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-vllm-g6-2b-q4w4a16emb4/benchmarks/runs/2026-11-05-engines-g6/REPORT.md" rel="noopener noreferrer"&gt;One-request run: 2026-11-05-engines-g6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-vllm-g6-2b-q4w4a16emb4/benchmarks/runs/2026-11-06-suite-g6/REPORT.md" rel="noopener noreferrer"&gt;Sweeps and accuracy: 2026-11-06-suite-g6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/instance-types/g6/" rel="noopener noreferrer"&gt;Amazon EC2 G6 instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ai/machine-learning/amis/" rel="noopener noreferrer"&gt;AWS Deep Learning AMIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sgl-project/sglang" rel="noopener noreferrer"&gt;SGLang&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/EricLBuehler/mistral.rs" rel="noopener noreferrer"&gt;mistral.rs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google-ai-edge/LiteRT-LM" rel="noopener noreferrer"&gt;LiteRT-LM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4" rel="noopener noreferrer"&gt;xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf" rel="noopener noreferrer"&gt;xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/grade-school-math" rel="noopener noreferrer"&gt;GSM8K&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gorilla.cs.berkeley.edu/leaderboard.html" rel="noopener noreferrer"&gt;Berkeley Function Calling Leaderboard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ec2</category>
      <category>vllm</category>
      <category>nvidia</category>
    </item>
    <item>
      <title>[Part 3] Your first steps in Computer Vision on AWS: deploying models for an ASL recognition app</title>
      <dc:creator>Darya Petrashka</dc:creator>
      <pubDate>Fri, 06 Nov 2026 15:54:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/part-3-your-first-steps-in-computer-vision-on-aws-deploying-models-for-an-asl-recognition-app-14hf</link>
      <guid>https://dev.to/aws-builders/part-3-your-first-steps-in-computer-vision-on-aws-deploying-models-for-an-asl-recognition-app-14hf</guid>
      <description>&lt;p&gt;This is the third and final part of the series "Your first steps in Computer Vision on AWS". If you are just starting, this tutorial will guide you step by step from the initial dataset to a deployed app.&lt;/p&gt;

&lt;p&gt;You can read &lt;a href="https://dev.to/dashapetr/part-1-your-first-steps-in-computer-vision-on-aws-asl-dataset-preprocessing-4h5p"&gt;the first part on ASL dataset + preprocessing&lt;/a&gt; and &lt;a href="https://dev.to/dashapetr/part-2-your-first-steps-in-computer-vision-on-aws-training-a-custom-cnn-model-1bjm"&gt;the second part on a custom CNN training&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  What we are trying to build
&lt;/h2&gt;

&lt;p&gt;While most ML tutorials end with model training and validation, we will take things one step further and deploy our models in an app. This is the most interesting and exciting part! We will make our models live! &lt;/p&gt;

&lt;p&gt;Here is what the AWS architecture will look like:&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%2Fujtow25j4x4xr644q5s9.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%2Fujtow25j4x4xr644q5s9.png" alt="ASL recognition app reference architecture" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a quick overview of the solution:&lt;/p&gt;

&lt;p&gt;The best way to test models is to take photos using a mobile phone. A simple static, serverless ASL Alphabet Recognition web application is hosted on S3 and accessed via a &lt;a href="https://aws.amazon.com/cloudfront/" rel="noopener noreferrer"&gt;CloudFront&lt;/a&gt; URL. To call the models for predictions, we will use &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt;. A &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-functions-chapter.html" rel="noopener noreferrer"&gt;Lambda event function&lt;/a&gt; will preprocess the uploaded photo and send it to a &lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/serverless-endpoints.html" rel="noopener noreferrer"&gt;serverless SageMaker inference endpoint&lt;/a&gt;. The endpoint will be created using a container stored in &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;ECR&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Most of the AWS infrastructure will be created automatically using &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt; (not shown on the architecture image). This will make the setup reproducible. However, we will first need to create the ECR repositories, build and push the SageMaker and Lambda container images, and upload the model archive to S3. The website files will also be uploaded to S3 manually after the stack is deployed.&lt;/p&gt;

&lt;p&gt;❓ &lt;em&gt;Why serverless endpoint?&lt;/em&gt;&lt;br&gt;
Our use case will have idle periods between traffic spurts and can tolerate cold starts. Serverless endpoints automatically launch compute resources and scale them in and out (even down to 0) depending on traffic, eliminating the need to choose instance types or manage scaling policies. With a pay-per-use model, Serverless Inference is a cost-effective option in our infrequent traffic pattern. The serverless endpoint supports a maximum memory size of 6144 MB (6 GB), which is more than enough for our three models.&lt;/p&gt;

&lt;p&gt;❓ &lt;em&gt;What would be the app cost?&lt;/em&gt;&lt;br&gt;
I spent less than $5 on the application. The serverless endpoint was running for around a week, but the cost was minimal. I spent more on the &lt;code&gt;ml.g5.xlarge&lt;/code&gt; instance in the previous part.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prepare models for inference
&lt;/h2&gt;

&lt;p&gt;⚠️ For this step, you can use your local environment or a SageMaker notebook instance (&lt;code&gt;ml.t3.medium&lt;/code&gt; is more than enough). &lt;/p&gt;

&lt;p&gt;Models trained in the previous part are ready: put &lt;code&gt;raw.keras&lt;/code&gt;, &lt;code&gt;filtered.keras&lt;/code&gt;, and &lt;code&gt;edges.keras&lt;/code&gt; into &lt;code&gt;models/&lt;/code&gt;&lt;br&gt;
You will also need to add the &lt;code&gt;class_names.json&lt;/code&gt; file to the &lt;code&gt;models/&lt;/code&gt; directory (&lt;a href="https://github.com/dashapetr/Computer_Vision_ASL_AWS/blob/main/models/class_names.json" rel="noopener noreferrer"&gt;copy/download it from the GitHub repo&lt;/a&gt;). &lt;br&gt;
It contains the exact class mapping used during training.&lt;/p&gt;

&lt;p&gt;From inside &lt;code&gt;models/&lt;/code&gt; directory run the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tar -czvf asl-models.tar.gz \
    raw.keras \
    filtered.keras \
    edges.keras \
    class_names.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get the archive &lt;code&gt;asl-models.tar.gz&lt;/code&gt;. Upload it to S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 cp asl-models.tar.gz \
    s3://YOUR-BUCKET/asl-app/models/asl-models.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Don't forget to replace the placeholder &lt;code&gt;YOUR-BUCKET&lt;/code&gt; and put your S3 bucket name!&lt;/p&gt;

&lt;p&gt;Once the archive has been uploaded to S3, we will use its S3 URI in CloudFormation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://YOUR-BUCKET/asl-app/models/asl-models.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build and push SageMaker inference container to ECR
&lt;/h2&gt;

&lt;p&gt;⚠️ For this step, you can use your local environment with AWS CLI (&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;How to install/update AWS CLI?&lt;/a&gt;) or a SageMaker notebook instance (&lt;code&gt;ml.t3.medium&lt;/code&gt; is more than enough). &lt;/p&gt;

&lt;p&gt;⚠️ Copy/download &lt;code&gt;inference.py&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, and &lt;code&gt;Dockerfile&lt;/code&gt; files from the &lt;a href="https://github.com/dashapetr/Computer_Vision_ASL_AWS/tree/main/sagemaker" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;. They should sit inside the &lt;code&gt;sagemaker&lt;/code&gt; directory (same structure as in the repo).&lt;/p&gt;

&lt;p&gt;The code in &lt;code&gt;inference.py&lt;/code&gt; is pretty simple: its main job is to handle inference for all three models.&lt;/p&gt;

&lt;p&gt;Now, create an ECR repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ecr create-repository \
    --repository-name asl-sagemaker-inference
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get your account ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws sts get-caller-identity \
    --query Account \
    --output text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export AWS_REGION=YOUR-REGION
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity \
    --query Account \
    --output text)

export ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ecr get-login-password \
    --region $AWS_REGION \
    | docker login \
    --username AWS \
    --password-stdin $ECR_URI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build and tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd sagemaker

docker build \
    -t asl-sagemaker-inference:latest .

docker tag \
    asl-sagemaker-inference:latest \
    $ECR_URI/asl-sagemaker-inference:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker push \
    $ECR_URI/asl-sagemaker-inference:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Make sure that your role or identity has the required permissions to do the job. Required actions: &lt;code&gt;ecr:InitiateLayerUpload&lt;/code&gt;, &lt;code&gt;ecr:UploadLayerPart&lt;/code&gt;, &lt;code&gt;ecr:CompleteLayerUpload&lt;/code&gt;, &lt;code&gt;ecr:BatchCheckLayerAvailability&lt;/code&gt;, and &lt;code&gt;ecr:PutImage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You will use in CloudFormation: &lt;br&gt;
&lt;code&gt;ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/asl-sagemaker-inference:latest&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Build Lambda image
&lt;/h2&gt;

&lt;p&gt;The main task of the Lambda function will be to accept an image from a phone, crop it to the center at 200×200 pixels, do the required preprocessing for the Gaussian-filtered model and Canny edge model and finally obtain predictions from all three models by invoking the SageMaker endpoint. For each model, it will get the top 3 predictions with their probabilities.&lt;/p&gt;

&lt;p&gt;⚠️ For this step, you can use your local environment with AWS CLI (&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;How to install/update AWS CLI?&lt;/a&gt;) or a SageMaker notebook instance (&lt;code&gt;ml.t3.medium&lt;/code&gt; is more than enough). &lt;/p&gt;

&lt;p&gt;⚠️ Copy/download &lt;code&gt;lambda_function.py&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, and &lt;code&gt;Dockerfile&lt;/code&gt; files from the &lt;a href="https://github.com/dashapetr/Computer_Vision_ASL_AWS/tree/main/lambda" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;. They should sit inside the &lt;code&gt;lambda&lt;/code&gt; directory (same structure as in the repo).&lt;/p&gt;

&lt;p&gt;Then, create a new ECR repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ecr create-repository \
    --repository-name asl-preprocessing-lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get your account ID and set variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws sts get-caller-identity \
    --query Account \
    --output text

export AWS_REGION=YOUR-REGION
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity \
    --query Account \
    --output text)

export ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ecr get-login-password \
    --region $AWS_REGION \
    | docker login \
    --username AWS \
    --password-stdin $ECR_URI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build, tag, and push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd lambda

docker buildx build \
    --platform linux/amd64 \
    --provenance=false \
    -t asl-preprocessing-lambda:latest \
    .

docker tag asl-preprocessing-lambda:latest \
    $ECR_URI/asl-preprocessing-lambda:latest

docker push $ECR_URI/asl-preprocessing-lambda:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will use the Lambda image in CloudFormation: &lt;br&gt;
&lt;code&gt;ACCOUNT.dkr.ecr.REGION.amazonaws.com/asl-preprocessing-lambda:latest&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploy the CloudFormation stack
&lt;/h2&gt;

&lt;p&gt;⚠️ For this step, you can use your local environment with AWS CLI (&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;How to install/update AWS CLI?&lt;/a&gt;) or a SageMaker notebook instance (&lt;code&gt;ml.t3.medium&lt;/code&gt; is more than enough). &lt;/p&gt;

&lt;p&gt;⚠️ Copy/download the &lt;code&gt;cloudformation.yaml&lt;/code&gt; file from the &lt;a href="https://github.com/dashapetr/Computer_Vision_ASL_AWS/blob/main/cloudformation.yaml" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This CloudFormation stack will create all the resources required for the serverless ASL Alphabet Recognition web application: CloudFront, S3 bucket, API Gateway, Lambda event function, SageMaker Serverless endpoint. As shown in the architecture diagram, CloudFront will route requests to &lt;code&gt;/api/*&lt;/code&gt; to API Gateway, giving you a single public URL for the application.&lt;br&gt;
Before deploying the stack, you will need to retrieve three values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&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;Model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3://YOUR-BUCKET/asl-app/models/asl-models.tar.gz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SageMaker image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ACCOUNT.dkr.ecr.REGION.amazonaws.com/asl-sagemaker-inference:latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ACCOUNT.dkr.ecr.REGION.amazonaws.com/asl-preprocessing-lambda:latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's deploy the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation deploy \
    --template-file cloudformation.yaml \
    --stack-name asl-recognizer \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides \
        ModelS3Uri=s3://YOUR-BUCKET/asl-app/models/asl-models.tar.gz \
        SageMakerImageUri=ACCOUNT.dkr.ecr.REGION.amazonaws.com/asl-sagemaker-inference:latest \
        LambdaImageUri=ACCOUNT.dkr.ecr.REGION.amazonaws.com/asl-preprocessing-lambda:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Make sure that your role or identity has the required permissions to successfully deploy your CloudFormation stack. Required actions: &lt;code&gt;cloudformation:DescribeStacks&lt;/code&gt;, &lt;code&gt;s3:CreateBucket&lt;/code&gt;, &lt;code&gt;s3:ListAllMyBuckets&lt;/code&gt;, &lt;code&gt;s3:GetBucketLocation&lt;/code&gt;, and &lt;code&gt;apigateway:POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It will take around 10-15 mins to complete. A good thing is that you can monitor stack events in &lt;a href="https://console.aws.amazon.com/cloudformation" rel="noopener noreferrer"&gt;the AWS CloudFormation Management Console&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%2Frjud503x22aisplsj9hw.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%2Frjud503x22aisplsj9hw.png" alt="Monitor events in the CloudFormation management console" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload the website
&lt;/h2&gt;

&lt;p&gt;⚠️ For this step, you can use your local environment with AWS CLI (&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;How to install/update AWS CLI?&lt;/a&gt;) or a SageMaker notebook instance (&lt;code&gt;ml.t3.medium&lt;/code&gt; is more than enough). &lt;/p&gt;

&lt;p&gt;⚠️ Copy/download &lt;code&gt;app.js&lt;/code&gt;, &lt;code&gt;index.html&lt;/code&gt;, and &lt;code&gt;styles.css&lt;/code&gt; files from the &lt;a href="https://github.com/dashapetr/Computer_Vision_ASL_AWS/tree/main/frontend" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;. They should sit inside the &lt;code&gt;frontend&lt;/code&gt; directory (same structure as in the repo).&lt;/p&gt;

&lt;p&gt;After CloudFormation finishes, retrieve the bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation describe-stacks \
    --stack-name asl-recognizer \
    --query \
    "Stacks[0].Outputs[?OutputKey=='FrontendBucket'].OutputValue" \
    --output text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will return a bucket name similar to &lt;code&gt;asl-recognizer-frontend-12000000&lt;/code&gt;. Use this value to upload your website pages from the &lt;code&gt;frontend&lt;/code&gt; directory to S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync frontend/ s3://asl-recognizer-frontend-12000000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, retrieve the CloudFront URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation describe-stacks \
    --stack-name asl-recognizer \
    --query \
    "Stacks[0].Outputs[?OutputKey=='WebsiteURL'].OutputValue" \
    --output text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get the CloudFront URL (e.g. &lt;code&gt;https://d1234567890abc.cloudfront.net&lt;/code&gt;). Paste it in the browser to access your application.&lt;br&gt;
As you can see, the web application's UI is very simple. Its main purpose is to let us test the models and get predictions:&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%2F4ctrztxnpqnnfqfq3z21.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%2F4ctrztxnpqnnfqfq3z21.png" alt="Minimalistic UI of the web app" width="300" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again, the provided &lt;a href="https://github.com/dashapetr/Computer_Vision_ASL_AWS" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; has all the required files for models inference preparation, SageMaker inference container creation, Lambda image creation, as well as website pages and the CloudFormation stack deployment. The repo structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asl-web-app/
│
├── models/
│   ├── raw.keras
│   ├── filtered.keras
│   ├── edges.keras
│   └── class_names.json
│
├── sagemaker/
│   ├── inference.py
│   ├── Dockerfile
│   └── requirements.txt
│
├── lambda/
│   ├── lambda_function.py
│   ├── Dockerfile
│   └── requirements.txt
│
├── frontend/
│   ├── index.html
│   ├── app.js
│   └── styles.css
│
└── cloudformation.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test the app
&lt;/h2&gt;

&lt;p&gt;The most exciting part begins! Use your phone to take photos and make predictions.&lt;br&gt;
First, I tried several images from the &lt;code&gt;asl_alphabet_test&lt;/code&gt; folder, which contains additional images provided alongside the training dataset.&lt;br&gt;
All three models performed reasonably well, producing the correct class predictions:&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%2F9swej22gi4i0817186l9.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%2F9swej22gi4i0817186l9.png" alt="Predicted label for 'C'" width="278" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, you may notice that the models' predictions on your own photos are not always correct. I tried to show a 'W': the model trained on raw RGB returned 'R', Gaussian-filtered and Canny edge models predicted 'K':&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%2Fz3ursak29jbbtn7renc6.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%2Fz3ursak29jbbtn7renc6.png" alt="Predicted labels for 'W'" width="350" height="934"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All our models achieved more than 90% accuracy on our held-out test split. They also performed well on additional images from Kaggle's separate &lt;code&gt;asl_alphabet_test&lt;/code&gt; folder, which were not included in the training, validation, or test splits. But when facing real-world images, models started to act weird. &lt;/p&gt;

&lt;p&gt;This reveals an important lesson about ML models and their applications outside the collected dataset. There is a gap between a toy dataset and real images; and we didn't fully account for it. If you look closer at training images, they all share a similar background, same skin tone, same hand size. Lightning and focus do vary, but not drastically. What if the person signing in ASL has a darker skin tone? What if we use a green/blue/orange background?&lt;/p&gt;

&lt;p&gt;In this project, we got too excited and proceeded with training using the data we had. And we are not alone. If you check the &lt;a href="https://www.kaggle.com/datasets/grassknoted/asl-alphabet/code?datasetId=23079&amp;amp;sortBy=voteCount" rel="noopener noreferrer"&gt;Kaggle dataset notebooks&lt;/a&gt;, many practitioners achieved almost 100% accuracy. The model works perfectly on the provided images, but its ability to generalize to real-world scenarios is not guaranteed.&lt;/p&gt;

&lt;p&gt;📖 For the future, we need to be aware of &lt;strong&gt;data cascades&lt;/strong&gt;: issues in data are 'cascaded' down the line, and the key strategy is to detect such data issues and work on them early on, before any model is trained. (Useful read: &lt;a href="https://research.google/pubs/everyone-wants-to-do-the-model-work-not-the-data-work-data-cascades-in-high-stakes-ai/" rel="noopener noreferrer"&gt;"Everyone wants to do the model work, not the data work": Data Cascades in High-Stakes AI&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;📖 It is very possible that our models relied on &lt;strong&gt;shortcut learning&lt;/strong&gt; (read &lt;a href="https://www.allpsych.uni-giessen.de/rauisch/readings/Geirhos_et_al.2020.pdf" rel="noopener noreferrer"&gt;Shortcut learning in deep neural networks&lt;/a&gt;). One way to investigate this is to use explainable AI methods, such as &lt;a href="https://arxiv.org/abs/1610.02391" rel="noopener noreferrer"&gt;Grad-CAM&lt;/a&gt;. It can help visualize which parts of an image influence a CNN's predictions.&lt;/p&gt;

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

&lt;p&gt;In this part, we deployed three CNN models to a single serverless SageMaker inference endpoint. We also used CloudFormation to provision the AWS resources needed to build a simple ASL Alphabet Recognition web application. Next, we tested the app with the test dataset images, as well as with our custom images. Finally, we found a gap between a toy Kaggle dataset and real-world conditions. The ability to overcome this gap is what distinguishes a simple proof of concept (POC) from a production-ready ML solution.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>computervision</category>
      <category>asl</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Beyond the Collector: How the Open-Source Observability Stack Fits Together</title>
      <dc:creator>hridyesh bisht</dc:creator>
      <pubDate>Fri, 06 Nov 2026 07:13:59 +0000</pubDate>
      <link>https://dev.to/aws-builders/beyond-the-collector-how-the-open-source-observability-stack-fits-together-l0j</link>
      <guid>https://dev.to/aws-builders/beyond-the-collector-how-the-open-source-observability-stack-fits-together-l0j</guid>
      <description>&lt;p&gt;In &lt;a href="https://programmerprodigy.code.blog/2025/06/04/understanding-observability-with-opentelemetry-and-coffee/" rel="noopener noreferrer"&gt;Understanding Observability with OpenTelemetry and Coffee&lt;/a&gt;, I introduced the fundamentals of observability. In &lt;a href="https://programmerprodigy.code.blog/2026/02/08/native-opetelemetry/" rel="noopener noreferrer"&gt;Native OpenTelemetry&lt;/a&gt;, I explored how OpenTelemetry generates and processes telemetry, how the OpenTelemetry Collector manages telemetry pipelines, and why understanding those pipelines matters when troubleshooting production issues.&lt;/p&gt;

&lt;p&gt;But what happens after telemetry leaves the Collector? Which tools store and analyze it? How do you connect these tools, and what should you consider before operating them in production?&lt;/p&gt;

&lt;p&gt;This article explores the open-source observability ecosystem, from the responsibilities of individual tools to the engineering decisions involved in running them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;OpenTelemetry isn't your observability backend&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry is an open-source, vendor-neutral observability framework. It provides APIs, SDKs, instrumentation libraries, and tools for generating, collecting, and exporting telemetry such as metrics, logs, and traces.&lt;/p&gt;

&lt;p&gt;It also defines common data formats and semantic conventions that help applications describe telemetry consistently. However, OpenTelemetry isn't an observability backend. It doesn't replace the storage, query interfaces, and dashboards that teams use to investigate production issues. Those capabilities come from other tools.&lt;/p&gt;

&lt;p&gt;Think of OpenTelemetry as the system that standardizes how information leaves each part of the coffee shop. The observability tools help you examine that information and understand what happened.&lt;/p&gt;

&lt;p&gt;Let's look at what each tool contributes to an observability stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Prometheus&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Prometheus commonly scrapes metrics endpoints exposed by applications and infrastructure. This pull-based model lets Prometheus collect measurements at configured intervals and discover targets through supported service-discovery mechanisms.&lt;/p&gt;

&lt;p&gt;Prometheus is useful when you need to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is request latency increasing?
&lt;/li&gt;
&lt;li&gt;Are error rates rising after a deployment?
&lt;/li&gt;
&lt;li&gt;Is an application approaching its CPU or memory limits?
&lt;/li&gt;
&lt;li&gt;Does the system meet its latency or availability targets?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You can also use alerting rules to detect conditions that require investigation.&lt;/p&gt;

&lt;p&gt;When integrating Prometheus with OpenTelemetry, decide how Prometheus will receive metrics. A common approach is to scrape an endpoint exposed by an application or the OpenTelemetry Collector's Prometheus exporter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://prometheus.io/docs/" rel="noopener noreferrer"&gt;Prometheus documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community:&lt;/strong&gt; &lt;a href="https://prometheus.io/community/" rel="noopener noreferrer"&gt;Prometheus community channels&lt;/a&gt;, including the #prometheus channel on CNCF Slack.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a href="https://discuss.prometheus.io/" rel="noopener noreferrer"&gt;Prometheus user forum&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Jaeger&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Jaeger provides a platform for exploring distributed traces and understanding how requests move through services.&lt;/p&gt;

&lt;p&gt;Jaeger is useful when metrics tell you that a service is slow but don't explain which operation causes the delay. You can inspect individual traces to investigate service dependencies, slow operations, and errors.&lt;/p&gt;

&lt;p&gt;When integrating Jaeger with OpenTelemetry, configure a compatible trace ingestion path, such as an appropriate OTLP endpoint. Also plan for trace storage and retention based on your deployment requirements. Jaeger uses a configured storage backend, so you must account for that backend in your architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://www.jaegertracing.io/docs/" rel="noopener noreferrer"&gt;Jaeger documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community:&lt;/strong&gt; &lt;a href="https://www.jaegertracing.io/get-in-touch/" rel="noopener noreferrer"&gt;Jaeger community channels&lt;/a&gt;, including the #jaeger channel on CNCF Slack.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a href="https://github.com/jaegertracing/jaeger/discussions" rel="noopener noreferrer"&gt;Jaeger GitHub Discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;OpenSearch&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;OpenSearch provides search and analytics capabilities for observability data, including application logs. Structured fields make it easier to filter records by service, environment, timestamp, or trace ID. For example, a log record might contain:&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;"service.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;"payment-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Payment provider timed out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"trace_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;"example-trace-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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fields such as &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;trace_id&lt;/code&gt; can help you narrow a search and connect a log entry to a related request. That connection depends on your instrumentation, log format, ingestion configuration, and the way you query the data.&lt;/p&gt;

&lt;p&gt;OpenSearch provides flexible search and analytics, but you need to configure ingestion, indexing, storage, and retention. For example, OpenSearch Data Prepper can process telemetry before sending it to OpenSearch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://docs.opensearch.org/latest/observing-your-data/" rel="noopener noreferrer"&gt;OpenSearch observability documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community:&lt;/strong&gt; &lt;a href="https://opensearch.org/connect/" rel="noopener noreferrer"&gt;Connect with the OpenSearch community&lt;/a&gt;, including its public Slack workspace.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a href="https://forum.opensearch.org/" rel="noopener noreferrer"&gt;OpenSearch community forum&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Perses&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Perses is an open-source dashboarding tool with an open dashboard specification and support for managing dashboards as code.&lt;/p&gt;

&lt;p&gt;Perses doesn't replace the systems that store telemetry. It queries configured data sources and displays their results. Its plugins determine which backends it can query. This separation lets teams manage dashboards independently of telemetry storage. However, dashboard definitions still depend on compatible data sources, queries, and configuration.&lt;/p&gt;

&lt;p&gt;For example, a team might configure Prometheus as a Perses data source to display request rates, error rates, and latency measurements. Other data sources depend on the available plugins and configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://perses.dev/docs/" rel="noopener noreferrer"&gt;Perses documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community:&lt;/strong&gt; &lt;a href="https://perses.dev/docs/perses/latest/contact/" rel="noopener noreferrer"&gt;Perses contact and community channels&lt;/a&gt;, including the #perses-dev channel on CNCF Slack.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a href="https://github.com/perses/perses/discussions" rel="noopener noreferrer"&gt;Perses GitHub Discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How do these tools fit together?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;An observability stack isn't one application. Installing these components doesn't automatically create a working observability stack. You must decide how data reaches each backend, which components process it, and how engineers access the results.&lt;/p&gt;

&lt;p&gt;The following diagram illustrates one possible architecture. It distinguishes the Prometheus scrape process from the trace and log ingestion paths.&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%2Fm4puv364zve8sw7nyu11.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%2Fm4puv364zve8sw7nyu11.png" alt="A diagram displaying how Open-Source Observability Stack Fits Together" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This architecture has three important parts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion:&lt;/strong&gt; Choose how each backend receives data. The Collector supports multiple receivers and exporters, but not every backend uses the same ingestion method.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage and analysis:&lt;/strong&gt; Decide which systems handle each data set. You might store metrics in Prometheus, configure a storage backend for Jaeger, and store logs in OpenSearch.

&lt;ul&gt;
&lt;li&gt;Routing data to multiple destinations is also possible when your requirements justify the additional processing and storage.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualization:&lt;/strong&gt; Decide where engineers will explore the data. Jaeger and OpenSearch provide their own interfaces. Perses supports reusable dashboards, but you must configure compatible data sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenTelemetry provides a foundation for telemetry transport. It doesn't automatically configure every backend, unify their interfaces, or correlate all the data they contain. Correlation also depends on consistent attributes, trace context, and compatible ingestion and query capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What happens when an incident occurs?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Consider a production service whose response latency suddenly increases. An engineer might follow this workflow:&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%2Fe72wli0fadl0od0qtug9.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%2Fe72wli0fadl0od0qtug9.png" alt="A diagram displaying what happens when an incident works with open telemetry" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Metrics help you detect and scope the problem. Traces help you locate the delay, and logs provide events that may explain it. After applying a fix, check the relevant telemetry to verify that the system has recovered.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Start with one use case&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You don't need to deploy every tool at once. Start with one observability question and build the smallest setup that can help you answer it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the question.&lt;/strong&gt; For example, determine whether a service's request latency is increasing.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose a backend.&lt;/strong&gt; Select a tool that supports the type of data you need to investigate.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send test telemetry.&lt;/strong&gt; Configure the required ingestion path and generate a small amount of test data.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the results.&lt;/strong&gt; Query the data and confirm that the expected measurements and attributes appear.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document what you learned.&lt;/strong&gt; Record the configuration, useful queries, and any limitations you encountered.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, you could start with Prometheus and query request latency for one service. Once you need to investigate individual requests, add tracing. If you also need to search detailed application events, introduce a log backend.&lt;/p&gt;

&lt;p&gt;This approach helps you validate each component before expanding the stack. It also makes integration problems easier to identify while the system is still small.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What should you consider before choosing your stack?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A successful experiment doesn't guarantee that a stack is ready for production. Before deploying it across services and environments, evaluate the integration work and operational responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Integration and query models&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Prometheus uses PromQL. OpenSearch supports its own query and analytics capabilities. Jaeger provides an interface for exploring traces, while Perses uses configured data sources and their respective query languages.&lt;/p&gt;

&lt;p&gt;Engineers may need to switch between tools to investigate the same incident. You also need to configure data sources, align resource attributes, and preserve trace context in logs when you want to connect related events.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Reliability, storage, and security&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each deployed component introduces operational requirements. Plan for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage and retention:&lt;/strong&gt; Estimate how data volume will grow and define retention policies for each signal.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability:&lt;/strong&gt; Decide how ingestion and investigation should behave when a backend becomes unavailable.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling:&lt;/strong&gt; Determine how to handle increasing telemetry volume and query load.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Control access to telemetry and protect sensitive information.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance:&lt;/strong&gt; Manage upgrades, configuration changes, backups, and recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These concerns become more important as the number of services, environments, and engineering teams grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Licensing and long-term flexibility&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Open-source projects provide different levels of freedom depending on their licenses. Some products follow an open-core model, offering an open-source base while reserving some capabilities for commercial editions.&lt;/p&gt;

&lt;p&gt;Before adopting a tool, review its license, governance, community activity, commercial feature boundaries, and data export options. Consider how difficult it would be to replace the component if your requirements change.&lt;/p&gt;

&lt;p&gt;The goal isn't to choose open-source software at any cost. It's to make an informed decision about flexibility, operational effort, and long-term sustainability.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Self-managed stack or unified observability platform?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You can assemble an observability stack from individual open-source tools or use a platform that brings telemetry together in one place. The right choice depends on your team's requirements, desired control, and capacity to operate the infrastructure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consideration&lt;/th&gt;
&lt;th&gt;Self-managed stack&lt;/th&gt;
&lt;th&gt;Unified observability platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Choose and configure individual components.&lt;/td&gt;
&lt;td&gt;Use the capabilities and configuration options provided by the platform.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your team connects and maintains the components.&lt;/td&gt;
&lt;td&gt;The platform provides an integrated experience for supported signals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your team manages the infrastructure, storage, upgrades, and maintenance.&lt;/td&gt;
&lt;td&gt;The platform can reduce the work required to operate separate backends.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customize components to meet specific requirements.&lt;/td&gt;
&lt;td&gt;Evaluate supported integrations, export options, and platform limitations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teams that need direct control and have the capacity to maintain the stack.&lt;/td&gt;
&lt;td&gt;Teams that prioritize a unified investigation experience and reduced operational work.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Neither approach is inherently better. A self-managed stack gives you control over individual components, while a unified platform can reduce integration and maintenance work, which allows you to focus on your business logic.&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%2Ffr54k8g6pwccqjwdqqcw.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%2Ffr54k8g6pwccqjwdqqcw.png" alt="A diagram displaying Dash0 integration with Open-Source Observability Stack" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Dash0: Explore telemetry in one platform&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Dash0 is an OpenTelemetry-native observability platform that brings metrics, logs, traces, and resource information together for analysis and investigation.&lt;/p&gt;

&lt;p&gt;For example, when a service's latency increases, you can examine metrics to understand the impact, inspect traces to identify slow operations, and investigate related logs to find potential causes within the same platform.&lt;/p&gt;

&lt;p&gt;Dash0 supports PromQL and uses the &lt;a href="https://www.dash0.com/docs/dash0/dashboards/about-dashboards" rel="noopener noreferrer"&gt;Perses dashboard specification&lt;/a&gt;. These open standards help teams reuse familiar query capabilities and dashboard workflows. Dash0 isn't a connector that automatically combines an existing Prometheus, Jaeger, OpenSearch, and Perses deployment. It's an alternative approach that can reduce the need to operate these components separately, depending on your requirements.&lt;/p&gt;

&lt;p&gt;Choose a self-managed stack when you need control over individual components and can maintain the infrastructure. Consider Dash0 when you want a unified place to investigate telemetry with less operational overhead.&lt;/p&gt;

&lt;p&gt;Learn more in the &lt;a href="https://www.dash0.com/docs/dash0" rel="noopener noreferrer"&gt;Dash0 documentation&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;An open-source observability stack is an architecture you design, not a set of tools that automatically work together.&lt;/p&gt;

&lt;p&gt;Prometheus, Jaeger, OpenSearch, and Perses address different needs. OpenTelemetry provides a foundation for instrumentation and telemetry transport, but you must configure the data paths, storage, integrations, and investigation workflows.&lt;/p&gt;

&lt;p&gt;You can assemble and maintain those components yourself, or evaluate a platform such as Dash0 to bring telemetry into a common observability experience. The best choice is the one that meets your requirements without introducing more operational complexity than your team can manage.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>observability</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Limits That Don't Limit</title>
      <dc:creator>Pin Xiong</dc:creator>
      <pubDate>Fri, 06 Nov 2026 00:43:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/the-limits-that-dont-limit-2o4j</link>
      <guid>https://dev.to/aws-builders/the-limits-that-dont-limit-2o4j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Verified on 2026-11-05 against &lt;code&gt;strands-agents&lt;/code&gt; 1.63.0, Python 3.11 and Bedrock in &lt;code&gt;us-east-1&lt;/code&gt;. Every number below came out of a run, and the setup steps live in &lt;code&gt;README.md&lt;/code&gt;. The model is &lt;code&gt;amazon.nova-pro-v1:0&lt;/code&gt; on Amazon Bedrock.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/posts/how-the-strands-agent-loop-works/" rel="noopener noreferrer"&gt;Article 01&lt;/a&gt; walked one cycle and counted thirteen stop reasons. Four of them start with &lt;code&gt;limit_&lt;/code&gt;: &lt;code&gt;limit_turns&lt;/code&gt;, &lt;code&gt;limit_output_tokens&lt;/code&gt;, &lt;code&gt;limit_total_tokens&lt;/code&gt; and &lt;code&gt;limit_structured_output_attempts&lt;/code&gt;. That post treated them as outcomes. This one is about how to cause them, and about how little each boundary guarantees once you look at what it measures. Read 01 first; everything here starts one level up, at the configuration you pass to a single call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent that never stops on its own
&lt;/h2&gt;

&lt;p&gt;Here is a tool that asks to be called again. It records one tick of a batch job, then reports how many remain.&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;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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockModel&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;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Record one tick of a batch job.

    Args:
        progress: how many ticks were already recorded before this call.

    Returns:
        The total recorded so far and how many remain in the batch.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&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;Recorded &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/8 ticks. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; remain. Call tick again with progress=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;done&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="k"&gt;return&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;Recorded &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/8 ticks. Batch complete.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run an agent on it with no limits and the loop finishes after 9 turns and 8 tool calls, at &lt;code&gt;end_turn&lt;/code&gt;. The ninth turn is the final answer, the one with no tool call in it. The reason it stopped is the last line of the tool output: the batch was complete. Nothing in the SDK decided that. If the tool had kept asking for another call, the loop would have kept serving it.&lt;/p&gt;

&lt;p&gt;That is the shape of the problem. An agent is a loop whose exit condition is a sentence the model reads, which is why four walls exist to replace that sentence with something you control. Two of them come from keys in the &lt;code&gt;limits&lt;/code&gt; dict below: &lt;code&gt;turns&lt;/code&gt; and the two token budgets. Two live outside it: a deadline is a manual &lt;code&gt;agent.cancel()&lt;/code&gt; call, and a quota is a hook. &lt;code&gt;structured_output_attempts&lt;/code&gt; is a fourth key in the same dict. It caps how many failed attempts at a structured response a request may accumulate (&lt;code&gt;types/agent.py:185&lt;/code&gt;), so it stays out of this 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="nf"&gt;agent&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="n"&gt;limits&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;turns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of this article is one section per boundary, because each one bounds something different from what its name suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a turn actually counts
&lt;/h2&gt;

&lt;p&gt;The type definition answers this in one line: a turn is "one model call plus any tool execution that follows" (&lt;code&gt;types/agent.py:177-178&lt;/code&gt;). So a turn limit counts model calls. It does not count tool calls, and the gap between the two is decided by the model, not by you.&lt;/p&gt;

&lt;p&gt;Two runs make the gap visible. Same tool, same eight calls, two prompts. A prompt that asks for one call at a time produces nine turns over eight calls. A prompt that permits several calls in one response produces three turns over the same eight calls.&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%2Fqhkpu5rned6n9vxo69uc.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%2Fqhkpu5rned6n9vxo69uc.png" alt="Same eight tool calls under two prompts: 0.9 calls per turn against 2.7" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;turns&lt;/code&gt; cap of 3 would have allowed the second run to finish its work and cut the first one off after three of them. The cap was identical. What it protected was not.&lt;/p&gt;

&lt;p&gt;So the cap itself is exact when it fires. With &lt;code&gt;turns=3&lt;/code&gt;, the loop stops with &lt;code&gt;limit_turns&lt;/code&gt; after three turns and three calls. With &lt;code&gt;turns=1&lt;/code&gt; it stops after one turn and one call, and the run ends without a summary. A cycle runs in a fixed order: check the limits, call the model, run the tools it asked for. The check sits at the top of that cycle, before the model call. That placement is what lets the source promise something useful. A cap that fires at the next cycle's top fires after the previous turn's tools completed. &lt;code&gt;agent.messages&lt;/code&gt; also stays valid for another call, with no model reply left pointing at tool calls that never ran (&lt;code&gt;types/agent.py:168-169&lt;/code&gt;, &lt;code&gt;event_loop/event_loop.py:217&lt;/code&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%2Forkc9c7ajmquhynu3f0e.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%2Forkc9c7ajmquhynu3f0e.png" alt="Three probes of the turn limit: no limits ends at 9 turns, turns=3 stops with limit_turns, turns=1 cuts after one turn" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In short, the practical reading is uncomfortable. A turn limit is a budget on model calls. If your cost driver is tool invocations, this wall is in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token cap that never sees the answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;output_tokens&lt;/code&gt; and &lt;code&gt;total_tokens&lt;/code&gt; look like the answer to that problem. They are documented as soft limits, and the docstring is blunt about why: a single oversized response can overshoot, more so when nested calls run in parallel (&lt;code&gt;types/agent.py:179-186&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The softness is larger than that sentence suggests, because both limits are checked in the same place as the turn cap, at a cycle boundary. A call that answers in one turn never reaches a boundary after the model speaks, so the check never sees the tokens it produced.&lt;/p&gt;

&lt;p&gt;One probe makes it concrete. An eighty-token output cap on a single-turn answer. The run produces 255 output tokens and stops with &lt;code&gt;end_turn&lt;/code&gt;, because a plain answer ends the loop before the check comes round again. The cap was not enforced. Its only comparison happened before the model spoke, against zero tokens.&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%2F6nnov5brc2airjsqfo4y.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%2F6nnov5brc2airjsqfo4y.png" alt="Two token probes: a single-turn answer sails past an 80-token cap with end_turn, a tool loop stops at limit_output_tokens" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Put a hundred-token cap on a loop and it binds, late but visibly. That cap stops the tick loop with &lt;code&gt;limit_output_tokens&lt;/code&gt; at 109 tokens, nine past the line, on the second turn. That overshoot is one part of one response, which is why the price of checking between turns is paid inside them.&lt;/p&gt;

&lt;p&gt;Total tokens compound faster, because every model call re-reads the whole conversation. In the same tick loop the input per cycle climbs 434, 559, 657, 755. Four cycles spend 2,648 tokens against a 2,600 cap and stop with &lt;code&gt;limit_total_tokens&lt;/code&gt;. That total is 2,405 input plus 243 output. The cap was set on total spend and the growth curve is what pushed through it.&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%2Fb5vpp966762xvtp817bc.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%2Fb5vpp966762xvtp817bc.png" alt="Token spend compounds: per-cycle input grows 434 to 755 and the total crosses the cap at 2648" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In other words, if you need a hard ceiling on one response, the &lt;code&gt;limits&lt;/code&gt; dict you pass to &lt;code&gt;agent()&lt;/code&gt; is the wrong tool. The model's own &lt;code&gt;max_tokens&lt;/code&gt; (a &lt;code&gt;BedrockModel&lt;/code&gt; parameter) caps a single call. Those limits check a conversation, and they check it one boundary late.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deadline that waits
&lt;/h2&gt;

&lt;p&gt;Wall-clock time is the boundary people assume is absolute. It is the most cooperative of the four. There is also no time key in the &lt;code&gt;limits&lt;/code&gt; dict: a deadline is a timer thread you own that calls &lt;code&gt;agent.cancel()&lt;/code&gt;. Here that thread is a watchdog.&lt;/p&gt;

&lt;p&gt;Two probes, same &lt;code&gt;agent.cancel()&lt;/code&gt;, different phases of the loop. In the first, the watchdog cancels three seconds into a long answer. Streaming is a cancellation safe point, so the call returns in 3.0 seconds with &lt;code&gt;stop_reason&lt;/code&gt; set to &lt;code&gt;cancelled&lt;/code&gt; and the cancel message as the final message from the model.&lt;/p&gt;

&lt;p&gt;In the second, the watchdog fires five seconds into a tool that sleeps twenty seconds. The tool keeps sleeping. The call returns after 21.9 seconds, still &lt;code&gt;cancelled&lt;/code&gt;, still carrying the message. The deadline was five seconds. The observed latency was more than four times that.&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%2Ft2vaplh8q90v0xftezex.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%2Ft2vaplh8q90v0xftezex.png" alt="Same cancel, two latencies: 3.0 seconds during streaming, 21.9 seconds inside a sync tool" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source lists the safe points: during model response streaming, before tool execution, during MCP tool execution, after tool execution and before the next model call (&lt;code&gt;agent/agent.py:686-690&lt;/code&gt;). A running synchronous tool is not on that list. Cancellation there means the agent stops as soon as control comes back.&lt;/p&gt;

&lt;p&gt;Nor does &lt;code&gt;cancelled&lt;/code&gt; undo what already ran. The loop appends the tool result to the messages and only then observes the cancellation (&lt;code&gt;event_loop/event_loop.py:1135&lt;/code&gt;, then &lt;code&gt;:1166&lt;/code&gt;), so the second probe above ends &lt;code&gt;cancelled&lt;/code&gt; with &lt;code&gt;[success] report ready: all checks green&lt;/code&gt; still sitting in &lt;code&gt;agent.messages&lt;/code&gt;. Cancellation is a statement about the loop, not about the side effect. If the tool can charge a card, &lt;code&gt;stop_reason&lt;/code&gt; alone will not tell you what happened: the retry path needs an idempotency key and a read-back of the effect before it runs again.&lt;/p&gt;

&lt;p&gt;So a deadline is only as good as the code under it. Record the deadline when the call starts and have the tool check &lt;code&gt;time.monotonic()&lt;/code&gt; against it or hand it to the timeout parameter of the HTTP client, database driver or subprocess. Without that, the wall you built has a door in it. The cancel message is worth using too: it becomes the text the caller sees, which beats a generic timeout error.&lt;/p&gt;

&lt;p&gt;The tool also has a cooperative way out. &lt;code&gt;ToolContext.cancel_signal&lt;/code&gt; is set by the loop on cancel, and the SDK documents the contract: poll it between steps or forward it to an API that accepts one, and "a tool that ignores it runs to completion" (&lt;code&gt;types/tools.py:191-195&lt;/code&gt;). The decorator has to opt in — write &lt;code&gt;@tool(context=True)&lt;/code&gt;, or the SDK rejects the signature with a &lt;code&gt;ValueError&lt;/code&gt; (&lt;code&gt;tools/decorator.py:183&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="nd"&gt;@tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&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;def&lt;/span&gt; &lt;span class="nf"&gt;slow_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolContext&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;str&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;tool_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel_signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aborted before doing any work: cancel signal received&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report ready: all checks green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same watchdog, same five-second deadline. The unaware tool holds the call for 21.7 seconds and still reports success; the aware one returns in 5.0 and the caller reads the abort note instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== A: cancel-unaware tool (time.sleep) ===
ELAPSED_S   : 21.7
STOP_REASON : cancelled
  [success] report ready: all checks green
=== B: cancel-aware tool (cancel_signal.wait) ===
ELAPSED_S   : 5.0
STOP_REASON : cancelled
  [success] aborted before doing any work: cancel signal received
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The quota the loop cannot enforce
&lt;/h2&gt;

&lt;p&gt;Taken together, none of the three boundaries above can enforce a count on how often one tool runs inside a single turn. The turn cap counts model calls, the token caps count tokens, and the deadline only stops tools that have not started yet: one already running keeps the loop waiting until it returns. A tool with side effects needs its own wall.&lt;/p&gt;

&lt;p&gt;The SDK exposes one through hooks. A &lt;code&gt;BeforeToolCallEvent&lt;/code&gt; callback fires before each tool call and can set &lt;code&gt;cancel_tool&lt;/code&gt;, which turns that call into a tool result with error status (&lt;code&gt;hooks/events.py:221-247&lt;/code&gt;). Count the calls yourself and stop at your number.&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;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.hooks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeforeToolCallEvent&lt;/span&gt;

&lt;span class="n"&gt;attempted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&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="c1"&gt;# one dict per request: reset it if you reuse the agent
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quota&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;BeforeToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&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;tool_use&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&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;cancel_tool&lt;/span&gt; &lt;span class="o"&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;quota exceeded: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; allows 2 calls per request, &lt;/span&gt;&lt;span class="sh"&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;this was attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&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="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BeforeToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a quota of two on the tick tool, the run makes three attempts: two execute, one is blocked, and the model reads the error and changes its answer. It reports two ticks recorded and explains the limit back to the caller. The loop then ends with &lt;code&gt;end_turn&lt;/code&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%2Fsqxgffm6s7b88gp166y0.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%2Fsqxgffm6s7b88gp166y0.png" alt="A quota hook: three attempts, two successes, one blocked call the model reads and adapts to" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last detail matters for how you place this wall, because the quota stops the tool, not the invocation. The model keeps its turn, sees a failure and decides what to do. For a payment API or a shell command, that is the right shape: the model gets a clear refusal it can reason about, and the loop is not killed mid-thought.&lt;/p&gt;

&lt;p&gt;One more boundary on this wall: the counter lives in one process. &lt;code&gt;attempted&lt;/code&gt; is a closure dict on one agent instance, so two workers pulling the same job each hold their own count and two plus two attempts all pass. A per-process quota bounds one run; it does not bound a job. If the job needs a ceiling, the count has to live in a shared store: reserve with a conditional write before the attempt, carry the attempt id as the idempotency key into the side effect, and release the reservation only after the effect is confirmed. Given what &lt;code&gt;cancelled&lt;/code&gt; does not undo, "the run was cancelled" is not grounds for releasing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four walls, one priority
&lt;/h2&gt;

&lt;p&gt;Because all four &lt;code&gt;limits&lt;/code&gt; checks run at the same cycle boundary, reaching several at once is the normal case rather than a rare one. The stop reason then follows a fixed order: &lt;code&gt;turns&lt;/code&gt; first, then &lt;code&gt;total_tokens&lt;/code&gt;, then &lt;code&gt;output_tokens&lt;/code&gt;, then &lt;code&gt;structured_output_attempts&lt;/code&gt; (&lt;code&gt;agent/invocation_context.py:229-262&lt;/code&gt;). Cross both &lt;code&gt;turns&lt;/code&gt; and &lt;code&gt;total_tokens&lt;/code&gt; in one cycle and the run reports &lt;code&gt;limit_turns&lt;/code&gt; only. Each check maps to its own &lt;code&gt;limit_*&lt;/code&gt; reason in the stop set (&lt;code&gt;types/event_loop.py:42-74&lt;/code&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%2Fyhq7a4p1z31u498j2fwb.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%2Fyhq7a4p1z31u498j2fwb.png" alt="The loop with four boundaries: where each check sits and what it bounds" width="800" height="699"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Misconfiguration is caught before any of this runs. The limits payload is validated when the request starts: unknown keys raise &lt;code&gt;TypeError&lt;/code&gt; with the accepted key list in the message, while zero, boolean and non-integer values raise &lt;code&gt;TypeError&lt;/code&gt; with the offending key and value (&lt;code&gt;agent/invocation_context.py:160-181&lt;/code&gt;). Three payloads, three errors, zero model calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: limits keys ['tuns'] are not recognized limits, expected one of 'turns', 'output_tokens', 'total_tokens', 'structured_output_attempts'
TypeError: limits['turns'] must be a positive int, got 0
TypeError: limits['turns'] must be a positive int, got True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typo in a limit name is the worst kind of boundary bug when nothing validates it, because the call succeeds and the wall is simply absent. Validation closes that hole.&lt;/p&gt;

&lt;p&gt;One operational footnote. Budgets restart on every top-level call: "Usage starts from zero on each top-level call; pass &lt;code&gt;result.invocation_context&lt;/code&gt; to a later call to continue on the same limits and usage" (&lt;code&gt;types/agent.py:165-166&lt;/code&gt;). Resume after a cancel or a limit stop with the same context and the budget continues; call again without it and the run starts fresh. And a budget inherited while already spent stops the call before the model runs: passing a context whose turn budget was exhausted returned &lt;code&gt;limit_turns&lt;/code&gt; with zero new tokens in &lt;code&gt;usage&lt;/code&gt; (&lt;code&gt;src/limits_reset.py&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;The official production guide links its tool quota advice to a "Limit Tool Counts" section of the hooks page. That anchor does not exist. The page has tool interception but no quota example, so the mechanism is real while the documented pattern is missing. The hook in the section above is built from &lt;code&gt;BeforeToolCallEvent.cancel_tool&lt;/code&gt; and verified by a run.&lt;/p&gt;

&lt;p&gt;My first pass at these experiments ran on &lt;code&gt;deepseek.v3.2&lt;/code&gt;, the model from Articles 01 and 02. It failed in a way worth reporting. Some runs printed the tool call as literal text, complete with the model's internal markup, instead of requesting a tool. The loop read a plain answer and ended. A turn cap cannot demonstrate anything on a run that stops at turn three for the wrong reason, so every experiment here is pinned to &lt;code&gt;amazon.nova-pro-v1:0&lt;/code&gt;. Boundary experiments are only as stable as the model's tool calling.&lt;/p&gt;

&lt;p&gt;The last surprise is granularity. Five identical runs, one call per turn, 0.9 calls per turn every time. Change one sentence of the prompt and the same eight calls fit into three turns. The number your turn cap protects against is partly a property of your prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use this
&lt;/h2&gt;

&lt;p&gt;A single-turn question and answer needs no turn cap. Adding one changes nothing that a &lt;code&gt;max_tokens&lt;/code&gt; setting would not fix better. The &lt;code&gt;limits&lt;/code&gt; budgets pay off in loops: retrieval agents, repair agents, anything that can call tools repeatedly.&lt;/p&gt;

&lt;p&gt;In contrast, a quota hook is for tools with cost or side effects. Putting one on a pure function adds a failure mode for no gain. A deadline is only worth setting where you can also propagate it downstream, because inside a blocking tool it is a suggestion.&lt;/p&gt;

&lt;p&gt;And do not treat these four as a checklist to fill in. Treat them as four different instruments, each with a known blind spot. Pick the one that covers the failure you actually fear.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pinxiong/strands-in-action.git
&lt;span class="nb"&gt;cd &lt;/span&gt;strands-in-action/usecases/03-execution-limits
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.11
&lt;span class="nv"&gt;VIRTUAL_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.venv uv pip &lt;span class="nb"&gt;install &lt;/span&gt;strands-agents&lt;span class="o"&gt;==&lt;/span&gt;1.63.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with AWS credentials that can call &lt;code&gt;amazon.nova-pro-v1:0&lt;/code&gt; in &lt;code&gt;us-east-1&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;python src/turn_limit.py          &lt;span class="c"&gt;# turn cap, three probes&lt;/span&gt;
python src/turn_batching.py       &lt;span class="c"&gt;# same calls, two prompts&lt;/span&gt;
python src/turn_granularity.py    &lt;span class="c"&gt;# five identical runs&lt;/span&gt;
python src/token_budget.py        &lt;span class="c"&gt;# single turn against a loop&lt;/span&gt;
python src/total_token_budget.py  &lt;span class="c"&gt;# compounding spend&lt;/span&gt;
python src/cancel_watchdog.py     &lt;span class="c"&gt;# cancellation latencies&lt;/span&gt;
python src/cancel_signal.py       &lt;span class="c"&gt;# cooperative cancel, no rollback&lt;/span&gt;
python src/limits_reset.py        &lt;span class="c"&gt;# budgets restart per top-level call&lt;/span&gt;
python src/tool_quota.py          &lt;span class="c"&gt;# quota hook&lt;/span&gt;
python src/limits_validation.py   &lt;span class="c"&gt;# no model calls needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;A turn is one model call plus its tools, so a turn cap bounds model calls, not tool calls.&lt;/li&gt;
&lt;li&gt;The same eight tool calls took 9 turns under one prompt and 3 under another.&lt;/li&gt;
&lt;li&gt;Token caps are checked at turn boundaries: a single-turn answer never reaches the check.&lt;/li&gt;
&lt;li&gt;A token cap can overshoot by part of one response, and &lt;code&gt;total_tokens&lt;/code&gt; compounds every turn.&lt;/li&gt;
&lt;li&gt;Cancellation is cooperative. A five-second deadline took 21.9 seconds inside a sync tool; a tool that waits on &lt;code&gt;tool_context.cancel_signal&lt;/code&gt; exits at the deadline instead.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;cancelled&lt;/code&gt; run can still hold the tool's success result. Cancellation never rolls the side effect back.&lt;/li&gt;
&lt;li&gt;A quota hook stops one tool call, returns an error the model can read and leaves the loop alive. Its counter bounds one process, not a job shared across workers.&lt;/li&gt;
&lt;li&gt;Limit budgets restart per top-level call; pass &lt;code&gt;result.invocation_context&lt;/code&gt; to continue them.&lt;/li&gt;
&lt;li&gt;Unknown limit keys fail loudly at request start, which is the only failure mode worth having.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>strands</category>
      <category>aws</category>
    </item>
    <item>
      <title>Gen AI Concepts for Bedrock</title>
      <dc:creator>عبدالله عياد | Abdullah Ayad</dc:creator>
      <pubDate>Thu, 05 Nov 2026 20:46:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/gen-ai-concepts-for-bedrock-3fhg</link>
      <guid>https://dev.to/aws-builders/gen-ai-concepts-for-bedrock-3fhg</guid>
      <description>&lt;h1&gt;
  
  
  1. Tokenization
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Definition:
&lt;/h3&gt;

&lt;p&gt;Tokenization is the process of breaking down text into smaller units, called tokens, which can be words, subwords, or even individual characters. These tokens serve as the building blocks that AI models use to process and generate language.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works:
&lt;/h3&gt;

&lt;p&gt;A sentence like "Artificial Intelligence is powerful."&lt;br&gt;
The model tokenizes it into smaller units:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Word-level tokenization:&lt;/code&gt;&lt;br&gt;
["Artificial", "Intelligence", "is", "powerful", "."]&lt;br&gt;
&lt;code&gt;Subword tokenization (e.g., Byte-Pair Encoding):&lt;/code&gt;&lt;br&gt;
["Artifi", "cial", "Intelli", "gence", "is", "power", "ful", "."]&lt;br&gt;
&lt;code&gt;Character-level tokenization:&lt;/code&gt;&lt;br&gt;
["A", "r", "t", "i", "f", "i", "c", "i", "a", "l", ...]&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Matters:
&lt;/h3&gt;

&lt;p&gt;Helps models understand and generate text efficiently.&lt;br&gt;
Handles out-of-vocabulary words by breaking them into subwords.&lt;br&gt;
Reduces complexity compared to processing raw text.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Context Window
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Definition:
&lt;/h3&gt;

&lt;p&gt;The context window refers to the maximum number of tokens an AI model can process at once when generating responses. It determines how much prior text the model "remembers" during inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works:
&lt;/h3&gt;

&lt;p&gt;If a model has a context window of 4,096 tokens, it can only consider the last 4,096 tokens when generating a response.&lt;br&gt;
Older parts of a conversation or document may be forgotten unless explicitly included in the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Matters:
&lt;/h3&gt;

&lt;p&gt;Short context windows (e.g., 512 tokens) limit long-form coherence.&lt;br&gt;
Larger context windows (e.g., 32,000+ tokens) improve memory and allow models to process entire documents in a single prompt.&lt;br&gt;
Optimizing context windows is crucial for applications like chatbots, legal analysis, and summarization.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Embeddings
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Definition:
&lt;/h3&gt;

&lt;p&gt;Embeddings are numerical representations of words, sentences, or documents in a high-dimensional space, allowing AI models to understand their relationships based on meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works:
&lt;/h3&gt;

&lt;p&gt;Instead of treating words as separate entities, embeddings represent them as vectors (lists of numbers) that capture their meanings.&lt;br&gt;
Similar words have embeddings that are closer together in space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example:
&lt;/h3&gt;

&lt;p&gt;"King" → [0.21, -1.32, 0.88, ...]&lt;br&gt;
"Queen" → [0.19, -1.29, 0.87, ...]&lt;br&gt;
"Dog" → [-0.75, 0.42, 0.33, ...]&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Matters:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Helps AI understand synonyms, context, and relationships between words.&lt;/li&gt;
&lt;li&gt;Used in search engines, recommendation systems, and NLP tasks to improve accuracy.&lt;/li&gt;
&lt;li&gt;Enables semantic search, where users can find relevant results even if they don’t use exact keywords.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/a00ayad00" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/a00ayad00" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
&lt;a href="https://m.facebook.com/a00ayad00" rel="noopener noreferrer"&gt;Facebook&lt;/a&gt;&lt;br&gt;
&lt;a href="https://a00ayad00.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>nlp</category>
    </item>
  </channel>
</rss>
