<?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>Kiro Crew: qué es y cómo empezar a usarlo</title>
      <dc:creator>Alvaro Llamojha</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:55:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/kiro-crew-que-es-y-como-empezar-a-usarlo-521g</link>
      <guid>https://dev.to/aws-builders/kiro-crew-que-es-y-como-empezar-a-usarlo-521g</guid>
      <description>&lt;h2&gt;
  
  
  Kiro Crew: qué es y cómo empezar a usarlo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/crew/" rel="noopener noreferrer"&gt;Kiro Crew&lt;/a&gt; es un &lt;em&gt;workspace&lt;/em&gt;&amp;nbsp;de desarrollo de código abierto y persistente que se ejecuta en tu propia máquina o en un entorno remoto. Mantiene contexto entre sesiones, permite ejecutar trabajo durante largos periodos, programar tareas recurrentes, coordinar varios agentes y utilizar el mismo entorno desde su dashboard, la aplicación de escritorio, el CLI o canales como Slack y Discord.&lt;/p&gt;

&lt;p&gt;Kiro Crew utiliza &lt;a href="https://kiro.dev/cli/" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt; como runtime y se comunica con él mediante ACP, Agent Client Protocol. Personalmente, esta es una evolución especialmente interesante porque ya estaba utilizando varias sesiones de Kiro CLI al mismo tiempo. Crew añade un entorno persistente alrededor de ese trabajo y agrupa capacidades que antes estaban repartidas entre terminales, scripts y otras herramientas.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;La parte más importante desde mi punto de vista es que Kiro Crew es &lt;a href="https://github.com/kirodotdev/KiroCrew" rel="noopener noreferrer"&gt;Código abierto y está disponible en GitHub&lt;/a&gt;, por lo que puedes revisar directamente su implementación, documentación y evolución. Puedes incluso crear tu propio fork de Kiro Crew a uno que se adapte más a tus necesidades.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh9r554duq2093nkaedtj.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%2Fh9r554duq2093nkaedtj.png" alt="Screenshot 1 - de la pantalla principal de Kiro Crew / dashboard con varias sesiones visibles" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En este artículo vamos a empezar desde cero: qué es Kiro Crew, cómo ponerlo en marcha y cuáles son las principales capacidades que merece la pena probar primero.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ¿Qué es Kiro Crew?
&lt;/h2&gt;

&lt;p&gt;El componente principal de Kiro Crew es el &lt;strong&gt;Gateway&lt;/strong&gt;. El Gateway es el proceso que conecta las diferentes interfaces de Crew con las sesiones de los agentes. Dashboard, desktop, CLI y los distintos canales de mensajería pueden utilizar el mismo Gateway, mientras que las sesiones ejecutan Kiro CLI mediante ACP y pueden acceder a herramientas adicionales mediante MCP.&lt;/p&gt;

&lt;p&gt;La &lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/architecture/overview.md" rel="noopener noreferrer"&gt;arquitectura de Kiro Crew&lt;/a&gt; se puede representar de una forma bastante sencilla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard / Desktop / CLI / Slack / Discord / ...
                         |
                         v
                      Gateway
                         |
                         v
                   Agent sessions
                         |
                         v
                 ACP + Kiro CLI + MCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiro Crew mantiene el estado de estas sesiones y permite ejecutar varias de forma concurrente. La documentación oficial también especifica que el contexto de proyectos puede sobrevivir entre sesiones y reinicios, que los trabajos largos utilizan checkpoints y que las tareas recurrentes pueden ejecutarse mediante schedules.&lt;/p&gt;

&lt;p&gt;La información persistente se almacena en el host donde ejecutas Crew. La instalación mantiene bajo &lt;code&gt;~/.kiro/crew/&lt;/code&gt; elementos como configuración, conversaciones, memoria, Knowledge, tareas programadas y agentes generados.&lt;/p&gt;

&lt;p&gt;Esto permite volver a sesiones anteriores y reutilizar información que ya existe&amp;nbsp;(contexto de la IA) dentro del workspace, en lugar de tratar cada interacción como una conversación completamente independiente.&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%2Fx7i2dbprwriepg6yfs91.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%2Fx7i2dbprwriepg6yfs91.png" alt="Screenshot 2 - de la vista de Sessions mostrando varias sesiones de Kiro Crew" width="640" height="1374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Instalar Kiro Crew
&lt;/h2&gt;

&lt;p&gt;Hay varias maneras de instalar Kiro Crew. Dispone de una aplicación de escritorio para macOS, Windows y Linux. También puede instalarse como paquete de Python, ejecutarse mediante Docker o construirse directamente desde el código fuente.&lt;/p&gt;

&lt;p&gt;La forma más sencilla de empezar es seguir la &lt;a href="https://kiro.dev/docs/crew/installation/" rel="noopener noreferrer"&gt;guía oficial de instalación de Kiro Crew&lt;/a&gt;. La aplicación de escritorio incluye el Gateway y puede ayudarte también con la instalación y autenticación de Kiro CLI durante el primer arranque.&lt;/p&gt;

&lt;p&gt;Si utilizas una instalación mediante paquete, código fuente o Docker, el flujo inicial incluye estos comandos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirocrew setup
kirocrew doctor
kirocrew gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kirocrew setup&lt;/code&gt; realiza la configuración inicial. &lt;code&gt;kirocrew doctor&lt;/code&gt; comprueba el estado de elementos como Kiro CLI, autenticación, embeddings, configuración y servidores MCP. &lt;code&gt;kirocrew gateway&lt;/code&gt; inicia el servidor, cuyo dashboard utiliza por defecto el puerto &lt;code&gt;5476&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Una vez iniciado el gateway, abre el dashboard; te pedirá si quieres importar la configuración de otros providers y ya estaría todo listo para lanzar una sesión. Cada conversación utiliza una sesión gestionada por Crew. &lt;/p&gt;

&lt;p&gt;Mi recomendación es empezar así: una sesión normal desde el dashboard, una tarea sencilla y comprobar cómo se conserva esa sesión antes de añadir automatizaciones o integraciones. Kiro Crew también tiene introspección, por lo que puedes hacer cualquier pregunta si te sientes perdido o no sabes qué más puedes configurar.&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%2Fbnilpvq3uwndlcyb5wzy.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%2Fbnilpvq3uwndlcyb5wzy.png" alt="Screenshot 3 - de una primera conversación/sesión funcionando en el dashboard" width="800" height="756"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Memory, Scheduled Tasks, Artifacts y Apps
&lt;/h2&gt;

&lt;p&gt;Una de las diferencias principales de Kiro Crew es que muchas de sus capacidades no están limitadas a una única conversación. Memory y Knowledge, Scheduled Tasks, Artifacts y Apps forman parte del mismo entorno persistente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory y Knowledge
&lt;/h3&gt;

&lt;p&gt;Kiro Crew conserva contexto entre sesiones y reinicios. Las correcciones pueden almacenarse como lessons y los workflows repetidos pueden convertirse en Skills en formato Markdown. También dispone de una Knowledge Library respaldada por búsqueda de texto y vector embeddings para recuperar información relevante.&lt;/p&gt;

&lt;p&gt;El sistema de embeddings se ejecuta en el propio Gateway. Durante el primer arranque, descarga el modelo de embeddings y, mientras no esté disponible, las búsquedas utilizan coincidencias por palabras clave. Cuando el modelo termina de descargarse, la búsqueda semántica pasa a utilizar embeddings sin necesidad de reiniciar Crew.&lt;/p&gt;

&lt;p&gt;Knowledge es una de las funcionalidades que más utilizaba ya en Kiro CLI. Poder conservar información de sesiones o proyectos y recuperarla después evita tener que volver a proporcionar el mismo contexto en cada nueva sesión.&lt;/p&gt;

&lt;p&gt;Knowledge puede además incorporar Artifacts y documentos para hacerlos recuperables posteriormente mediante búsqueda.&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%2Fw1kliertcfne1tuw4xxq.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%2Fw1kliertcfne1tuw4xxq.png" alt="Screenshot 4 - de Kiro Crew Knowledge Library" width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scheduled Tasks
&lt;/h3&gt;

&lt;p&gt;Kiro Crew permite ejecutar trabajo de forma programada utilizando cron jobs. En la &lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/architecture/mcp.md" rel="noopener noreferrer"&gt;arquitectura MCP de Kiro Crew&lt;/a&gt; aparecen herramientas específicas para crear, consultar, actualizar, pausar, reanudar y ejecutar tareas programadas.&lt;/p&gt;

&lt;p&gt;En mi caso, uso el schedule task para recopilar artículos sobre Kiro para mi Kiro Weekly Reading List. Cada 2 días ejecuta un script que encuentra artículos y los pone en una lista que luego al final de la semana lo proceso.&lt;/p&gt;

&lt;p&gt;Ese workflow con su script es mío y ya lo usaba antes de manera manual por Kiro Web. Lo que aporta Crew es la infraestructura para programar ese trabajo y ejecutarlo como una sesión gestionada sin que yo tenga que iniciar manualmente la tarea cada vez.&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%2F2l6vm18fohj2t3u4sdbu.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%2F2l6vm18fohj2t3u4sdbu.png" alt="Screenshot 5 - de Scheduled Tasks mostrando al menos una tarea configurada y su siguiente ejecución" width="800" height="874"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Artifacts
&lt;/h3&gt;

&lt;p&gt;Kiro Crew dispone de &lt;strong&gt;Artifacts persistentes&lt;/strong&gt; para conservar resultados producidos durante las sesiones. Sus herramientas permiten guardar, consultar, modificar y mantener versiones de esos resultados.&lt;/p&gt;

&lt;p&gt;Esto puede utilizarse para documentación, especificaciones, informes, investigaciones u otros archivos que quieras conservar más allá de la conversación que los generó.&lt;/p&gt;

&lt;p&gt;Soy un defensor de cómo manejar efectivamente el contexto de la IA; en el caso de Kiro, son Steerings, Skills, Powers y, con Kiro Crew ahora también son los Artifacts. En mi caso, esto es especialmente útil porque genero reportes y documentación en Markdown o HTML que se pueden guardar como artifacts en Kiro Crew. &lt;/p&gt;

&lt;p&gt;Artifacts también puede integrarse con Knowledge, haciendo que determinados documentos producidos durante una sesión puedan incorporarse posteriormente a la base de conocimiento de Crew.&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%2F01qwhv1aa2wmy63g0lj0.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%2F01qwhv1aa2wmy63g0lj0.png" alt="Screenshot 6- de la vista de Artifacts con varios documentos" width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Apps: mucho más que un chat
&lt;/h3&gt;

&lt;p&gt;Una de las partes que más me ha llamado la atención de Kiro Crew son las &lt;a href="https://kiro.dev/docs/crew/apps/" rel="noopener noreferrer"&gt;Apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Una App puede combinar agentes, Skills, servidores MCP, cron jobs, páginas personalizadas dentro del dashboard y procesos backend. Esto permite crear experiencias específicas para una tarea sin que toda la interacción tenga que ocurrir dentro de una conversación.&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%2Fm4p9osneol1qtirawst4.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%2Fm4p9osneol1qtirawst4.png" alt="Screenshot 7 - del catálogo/App Store de Kiro Crew" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Por ejemplo, una app puede proporcionar una interfaz propia mientras utiliza agentes y automatizaciones por debajo. Entre los ejemplos mostrados por Kiro aparecen aplicaciones orientadas a investigación, revisión de código y otras tareas especializadas.&lt;/p&gt;

&lt;p&gt;Kiro Crew también incluye un &lt;a href="https://kiro.dev/docs/crew/apps/sdk/" rel="noopener noreferrer"&gt;App SDK&lt;/a&gt;. Las interfaces del dashboard pueden construirse con TypeScript y React, mientras que otros servicios pueden interactuar con Crew mediante el cliente Python o las APIs REST y WebSocket del Gateway.&lt;/p&gt;

&lt;p&gt;Las apps pueden ir desde algo pequeño basado en una Skill hasta aplicaciones con agentes, backend y una interfaz completa. También pueden distribuirse mediante registros o repositorios en Github.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Seguridad, Channels y ejecución remota
&lt;/h2&gt;

&lt;p&gt;Permitir que un agente continúe trabajando cuando no estamos delante requiere controlar qué acciones puede ejecutar. Kiro Crew incorpora varias capas de seguridad que incluyen aprobación de herramientas, protección de paths sensibles, comandos denegados, sandboxing del proceso, validación de inputs MCP y redacción de credenciales.&lt;/p&gt;

&lt;p&gt;La &lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/architecture/security-deep-dive.md" rel="noopener noreferrer"&gt;documentación de seguridad de Kiro Crew&lt;/a&gt; describe además una separación entre el Gateway, Kiro CLI y los procesos utilizados por las herramientas.&lt;/p&gt;

&lt;p&gt;Las peticiones de herramientas pasan por un &lt;strong&gt;PreToolUse gate&lt;/strong&gt; gestionado por Crew antes de ejecutarse. Esto permite aplicar las políticas de ejecución fuera del proceso controlado por el agente.&lt;/p&gt;

&lt;p&gt;Con Kiro CLI ya tenía determinadas operaciones que permitía ejecutar y otras que prefería aprobar manualmente, aún no confiaba en una sesión totalmente automática (el famoso modo YOLO). Kiro Crew y sus capas de seguridad aumentan mi confianza en ejecutarlo en modo permisivo (YOLO). Mi recomendación es probar este modo con un par de tareas y monitorizándolo, una vez ganes confianza en este sistema, activar el modo auto-approve solo por unas horas.&amp;nbsp;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmgx0fi2huqfz2vc9g5j.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%2Fbmgx0fi2huqfz2vc9g5j.png" alt="Screenshot 8 - de Security Settings / Tool Approvals / Denied Commands de Kiro Crew" width="799" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Channels
&lt;/h3&gt;

&lt;p&gt;Kiro Crew no obliga a interactuar únicamente desde el dashboard. Soporta integraciones con distintos sistemas de mensajería, incluyendo Slack, Discord, Telegram y Microsoft Teams, entre otros.&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%2F3yrrenjetkq2joau9cfc.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%2F3yrrenjetkq2joau9cfc.png" alt="Screenshot 9 - de la pantalla de configuración de Channels mostrando las integraciones disponibles" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Los mensajes recibidos desde estos canales se conectan con sesiones gestionadas por Kiro Crew y las integraciones pueden incluir también el flujo de aprobaciones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ejecución remota
&lt;/h3&gt;

&lt;p&gt;Ahora que puedes usar Kiro desde distintos canales, el siguiente paso es tener Kiro Crew corriendo en remoto. Kiro Crew permite ejecutarse en un host remoto (como en AWS) o mediante Docker. Esto permite mantener el Gateway disponible sin depender de que tu máquina principal permanezca encendida y tenerla disponible desde tus canales.&lt;/p&gt;

&lt;p&gt;El proyecto incluye además soporte específico para AWS mediante &lt;strong&gt;Cloud Launcher&lt;/strong&gt;. Kiro Crew puede crear una instancia EC2 utilizando CloudFormation, configurar el entorno y permitir posteriormente el acceso utilizando AWS Systems Manager.&lt;/p&gt;

&lt;p&gt;La &lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/system-specs/modules/cloud.md" rel="noopener noreferrer"&gt;documentación del Cloud Launcher&lt;/a&gt; incluye comandos como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirocrew cloud doctor &lt;span class="nt"&gt;--region&lt;/span&gt; eu-central-1
kirocrew cloud launch &lt;span class="nt"&gt;--region&lt;/span&gt; eu-central-1
kirocrew cloud status
kirocrew cloud connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;También existen operaciones para detener, arrancar y destruir el entorno remoto. Muy útil para casos donde no estés usando Kiro Crew a cada hora del día.&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. ¿Qué probar primero?
&lt;/h2&gt;

&lt;p&gt;Kiro Crew tiene muchas capacidades y es fácil sobrecargarse con tantas cosas que configurar  desde el primer día. Empezaría con algo mucho más sencillo.&lt;/p&gt;

&lt;p&gt;Primero, crearía una conversación desde el dashboard, organizaría carpetas y workspaces, me familiarizaría con los modos de aprobación y empezaría con un par de sesiones. Después, en una sesión en el mismo Kiro Crew, le pediría que llevara tu knowledge existente, agents y skills a Kiro Crew. Si no tienes, le pediría que creara nuevas basándose en tus necesidades.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;También probaría alguna de las apps disponibles. Ahora mismo estoy probando la nueva app de AWS Control donde conectar mis cuentas de AWS y ver sus gastos.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;La mejor forma de entender Kiro Crew es utilizarlo. Puedes empezar directamente desde la &lt;a href="https://kiro.dev/crew/" rel="noopener noreferrer"&gt;página oficial de Kiro Crew&lt;/a&gt;, consultar su &lt;a href="https://kiro.dev/docs/crew/" rel="noopener noreferrer"&gt;documentación&lt;/a&gt; o revisar y ejecutar el código desde el &lt;a href="https://github.com/kirodotdev/KiroCrew" rel="noopener noreferrer"&gt;repositorio oficial de Kiro Crew en GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>kirocrew</category>
      <category>ai</category>
      <category>aws</category>
    </item>
    <item>
      <title>Agentic AI Development with Kiro: The Hidden DevSecOps Layer — Closing the Loop</title>
      <dc:creator>Alejandro Velez</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:56:33 +0000</pubDate>
      <link>https://dev.to/aws-builders/agentic-ai-development-with-kiro-the-hidden-devsecops-layer-closing-the-loop-3657</link>
      <guid>https://dev.to/aws-builders/agentic-ai-development-with-kiro-the-hidden-devsecops-layer-closing-the-loop-3657</guid>
      <description>&lt;p&gt;&lt;em&gt;Level 300&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Some time ago we created a &lt;a href="https://dev.to/aws-builders/agentic-ai-development-with-kiro-from-zero-to-saas-platform-30d6"&gt;blog&lt;/a&gt; showing the capabilities of AI DLC and SDD to create quick and efficient prototypes as a MVP, the results were amazing, however, we omitted something: DevSecOps best practices and CICD for the workload.&lt;/p&gt;

&lt;p&gt;The prototype was built with a serverless framework and modern cloud-native application patterns. However, moving from a working MVP to a production-ready solution requires stronger alignment with DevSecOps, CI/CD, and operational excellence. That transition leaves several important questions open:&lt;/p&gt;

&lt;p&gt;Security posture&lt;br&gt;
• What security vulnerabilities exist in the solution?&lt;br&gt;
• Is the platform ready to withstand common web attacks?&lt;br&gt;
Code quality and production readiness&lt;br&gt;
• What is the overall quality of the code?&lt;br&gt;
• Is this truly an example of an enterprise-ready solution?&lt;br&gt;
Cloud compliance and misconfiguration risk&lt;br&gt;
• Which cloud security compliance gaps still need to be addressed?&lt;br&gt;
• Are there misconfigurations that could create operational or security risk?&lt;/p&gt;

&lt;p&gt;The other side of this challenge is organizational readiness. Many companies are enabling development teams with assistants such as Kiro, Claude, Cursor, and similar tools. However, without a mature process to &lt;strong&gt;review, scan, govern, and manage code at scale, these tools can introduce high costs, expand security risks, and growing technical debt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the Agentic AI era, DevSecOps maturity is no longer optional. A secure software development lifecycle, policy-driven development, and zero-trust principles must become core operating requirements rather than afterthoughts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Open loop – Starter point a classical DevSecOps CICD system
&lt;/h2&gt;

&lt;p&gt;Suppose that the company already has a continuous integration and delivery framework and tools for that kind of workload with classical tools, in terms of maturity level and L3 Defined and Managed here we have Security gates integrated in CI/CD; SAST/SCA/secrets/IaC; centralized findings; quality gates.&lt;/p&gt;
&lt;h2&gt;
  
  
  The maturity model we use
&lt;/h2&gt;

&lt;p&gt;We measure against a five-level DevSecOps maturity model. The levels are&lt;br&gt;
cumulative — each assumes the previous.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Characteristic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;Initial&lt;/td&gt;
&lt;td&gt;Manual builds/deploys; security is a late, manual gate (if any).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;Repeatable&lt;/td&gt;
&lt;td&gt;Scripted CD; some automated tests; ad-hoc scanning.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;Defined / Managed&lt;/td&gt;
&lt;td&gt;Security gates integrated in CI/CD; SAST/SCA/secrets/IaC; centralized findings; quality gates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;Agent-Integrated&lt;/td&gt;
&lt;td&gt;Pipelines dispatch agents to remediate; findings become PRs; developers review instead of investigate; feedback loops close.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;Autonomous / Self-Healing&lt;/td&gt;
&lt;td&gt;Agents act on production/runtime signals with policy-bounded autonomy; the system proposes and validates its own improvements.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Solid DevSecOps Level 3&lt;br&gt;
Here is what is genuinely in place. Most organizations &lt;em&gt;never&lt;/em&gt; achieve this.&lt;/p&gt;
&lt;h3&gt;
  
  
  Delivery
&lt;/h3&gt;

&lt;p&gt;• Two independent, path-filtered CI/CD pipelines — backend (CDK/Lambda) and frontend (React/S3/CloudFront), separated because their lifecycles differ.&lt;br&gt;
o   Backend: backend-{development,staging,production}.yml&lt;br&gt;
o   Frontend: frontend-{development,staging,production}.yml&lt;br&gt;
• Reusable workflows for security scanning and deployment per component.&lt;br&gt;
• Environment promotion: dev → staging (on success) → production (on release).&lt;br&gt;
• OIDC hub/spoke AWS auth — no long-lived cloud credentials in CI.&lt;/p&gt;
&lt;h3&gt;
  
  
  Security gates (the DevSecOps core)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secret scanning&lt;/td&gt;
&lt;td&gt;gitleaks + git-secrets&lt;/td&gt;
&lt;td&gt;every pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAST&lt;/td&gt;
&lt;td&gt;Semgrep + SonarQube&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sast&lt;/code&gt; job, per component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCA + SBOM&lt;/td&gt;
&lt;td&gt;Trivy → CycloneDX → Dependency-Track&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sca&lt;/code&gt; job, per component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC / config&lt;/td&gt;
&lt;td&gt;thothctl (checkov, trivy)&lt;/td&gt;
&lt;td&gt;backend &lt;code&gt;iac&lt;/code&gt; job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Findings aggregation&lt;/td&gt;
&lt;td&gt;SecObserve&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gate&lt;/code&gt; job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality gate&lt;/td&gt;
&lt;td&gt;SonarQube "Athleon Industry Standard" (Clean-as-You-Code)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sast&lt;/code&gt; job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-component SonarQube projects&lt;/strong&gt;: &lt;code&gt;athleon-backend&lt;/code&gt;, &lt;code&gt;athleon-frontend&lt;/code&gt;,
both on a Clean-as-You-Code quality gate (0 new issues, ≥80% new coverage,
≤3% new duplication, A ratings, 100% new hotspots reviewed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SBOMs published to Dependency-Track&lt;/strong&gt; per environment
(&lt;code&gt;athleon-backend-&amp;lt;env&amp;gt;&lt;/code&gt;, &lt;code&gt;athleon-frontend-&amp;lt;env&amp;gt;&lt;/code&gt;), CycloneDX normalized to
the version DT accepts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All scanner findings imported into SecObserve&lt;/strong&gt; for centralized triage.&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%2Fjqaf08jcjsvqbmc2jq72.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%2Fjqaf08jcjsvqbmc2jq72.png" alt="DevSecOps Classical Pipeline" width="800" height="1007"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Kiro adoption (the differentiator)
&lt;/h3&gt;

&lt;p&gt;The company enables the AI adoption for devs centric on Kiro adoption (the differentiator)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec-driven development&lt;/strong&gt; is an established muscle: six specs under
&lt;code&gt;.kiro/specs/&lt;/code&gt; with requirements / design / tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steering docs&lt;/strong&gt; (&lt;code&gt;.kiro/steering/product.md&lt;/code&gt;, &lt;code&gt;structure.md&lt;/code&gt;, &lt;code&gt;tech.md&lt;/code&gt;)
encode product and architecture context for the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One remediation sub-agent&lt;/strong&gt; (&lt;code&gt;.kiro/agents/sonar-fixer.json&lt;/code&gt;) wired to the
official SonarQube MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP configured&lt;/strong&gt; for Kiro CLI and IDE (&lt;code&gt;.kiro/settings/mcp.json&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point the hard part is done: the gates, the aggregation, the quality bar, and — crucially — a team fluent in Kiro's spec-driven workflow. The next level is not more tools. It's connecting the tools we have to the agents we have, so the pipeline stops reporting problems to humans and starts handing humans review-ready fixes.&lt;/p&gt;

&lt;p&gt;The number of findings was significant:&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%2Fjrrr1mnab6is7b2vkn3h.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%2Fjrrr1mnab6is7b2vkn3h.png" alt="SecObserve Unify" width="800" height="463"&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%2F3gzn34tahhyvijig03va.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%2F3gzn34tahhyvijig03va.png" alt="SonarQube Scan" width="799" height="263"&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%2F4n0v7y8q5nnx5ai4ov4r.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%2F4n0v7y8q5nnx5ai4ov4r.png" alt="DependencyTrack" width="799" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Also, github security and quality report 344 issues&lt;/p&gt;
&lt;/blockquote&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%2Fgwmr0ci95sjt3c2f7kp2.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%2Fgwmr0ci95sjt3c2f7kp2.png" alt="Github Pipeline" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The devs could manually interact with agent and sonar to improve the code and fix the issues, however, is a manual approach. Running the sonarfixer agent that uses the MCP tools from shell interactive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kiro-cli chat &lt;span class="nt"&gt;--agent&lt;/span&gt; sonar-fixer &lt;span class="s2"&gt;"Fix the top criticals on athleon-backend"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the MCP and agent:&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;"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;"sonar-fixer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SonarQube remediation specialist for Athleon - queries SonarQube via MCP and fixes code quality, reliability, and security issues following Clean-as-You-Code."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&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 are the SonarQube remediation specialist for the Athleon platform (multi-tenant calisthenics competition management: AWS CDK, Node.js Lambda, React/Vite frontend). Your job is to reduce SonarQube findings safely and systematically.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Project key: athleon. SonarQube Server: http://sonar.devsecops.lan.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Current baseline (improve, do not regress): Security rating D (271 issues), Reliability E (~1.2k), Maintainability A (~2k), Coverage 0-4%, Duplications ~21.7%. The active quality gate 'Athleon Industry Standard' enforces Clean-as-You-Code on NEW code (0 new issues, &amp;gt;=80% new coverage, &amp;lt;=3% new duplication, A ratings, 100% new hotspots reviewed).&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;WORKING PRINCIPLES:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;1. Use the SonarQube MCP tools to fetch real issues - never guess. Prioritize: (a) BLOCKER/CRITICAL security &amp;amp; reliability first, (b) then high-impact maintainability, (c) then duplication.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;2. Fix in small, reviewable batches grouped by rule or file. One rule family per change set.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;3. Before editing, read the surrounding code and the project's steering docs (.kiro/steering/) to match conventions. Follow existing patterns (shared layer utils, DDD bounded contexts, EventBridge cross-domain comms - never direct cross-domain imports).&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;4. For every fix: explain the rule, why it fired, and the fix. Preserve behavior. Do NOT introduce new dependencies without asking.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;5. Add or update tests for any behavior you touch (vitest). Raising coverage on changed files helps the new-code gate.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;6. Security issues: apply secure patterns (input validation, parameterized queries, no hardcoded secrets, least privilege). Never weaken auth/RBAC.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;7. Verify after each batch: run the relevant vitest suite and, if possible, re-check the issue in SonarQube.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;8. NEVER mark issues as false-positive/won't-fix in SonarQube without explicit human approval.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;9. Respect the safety guardrails: no destructive git ops, no production changes, confirm high-impact actions.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;WORKFLOW per session:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- Ask which scope to work on (a domain, a file, a rule, or 'top criticals') if not specified.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- Query SonarQube for that scope. Present a short prioritized plan.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- Implement the highest-value batch. Run tests. Summarize what changed and the expected SonarQube impact.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- Repeat or hand back for review."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"sonarqube"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--init"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--pull=always"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--network=host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"SONARQUBE_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"SONARQUBE_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;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"SONARQUBE_PROJECT_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/.kiro/.sonar-mcp-storage:/app/storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}:/app/mcp-workspace:ro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sonarsource/sonarqube-mcp"&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;"env"&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;"SONARQUBE_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_SONARQUBE_USER_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"SONARQUBE_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;"http://sonar.devsecops.lan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"SONARQUBE_PROJECT_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"athleon-backend"&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;"autoApprove"&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;"disabled"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs_read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs_write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"execute_bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@sonarqube"&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;"allowedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs_read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@sonarqube"&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;"toolsSettings"&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;"execute_bash"&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;"allowedCommands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"npx vitest*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"npm test*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"npm run test*"&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;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenvecul0zvlvi334pn6p.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%2Fenvecul0zvlvi334pn6p.png" alt="Kiro interactive Mode" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this stage, the key question becomes: &lt;strong&gt;how can DevSecOps teams evolve the existing platform into an agent-integrated DevSecOps model?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Level 4 Vision: Agent-Integrated DevSecOps
&lt;/h2&gt;

&lt;p&gt;Instead of simply alerting humans to problems, the pipeline should deliver review-ready fixes.&lt;br&gt;
Today, the developer workflow for security or quality finding typically looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; pipeline fails gate
  → developer gets notified
  → opens SonarQube / SecObserve / Dependency-Track to understand it
  → context-switches into the IDE
  → reproduces, investigates, fixes
  → writes tests
  → opens PR

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's minutes-to-hours of human context switching per finding, and it scales linearly with the number of findings. It is the single biggest drag on time-to-market for a team that already has good gates.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Level 4 loop:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipeline finding issue
  → dispatches the right agent (headless Kiro CLI, using an existing sub-agent)
  → agent reads the finding via MCP, fixes it, writes/updates tests, runs them
  → agent opens a PR with an explanation
  → developer REVIEWS the PR (approve / request changes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Humans are still in the loop — but at the review step, which is where human judgment is most valuable and least repetitive. This is the core TTM (Time to Marker) lever.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "closing the loop" requires
&lt;/h3&gt;

&lt;p&gt;Four connections must exist that don’t exist today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Trigger → Agent&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A pipeline event (failed quality gate, new critical vuln, new SecObserve finding) must be able to dispatch an agent without a human. Kiro CLI supports this directly:&lt;/p&gt;

&lt;p&gt;• &lt;code&gt;kiro-cli chat --no-interactive --agent sonar-fixer "..."&lt;/code&gt; runs headless.&lt;br&gt;
• &lt;code&gt;KIRO_API_KEY&lt;/code&gt; authenticates in CI with no browser flow.&lt;br&gt;
• Standard exit codes (0/1/2) make it scriptable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agent → Context (the MCP mesh)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An agent is only as good as what it can see. Today the sonar-fixer sees SonarQube. A Level-4 agent should see the whole security picture: SonarQube (have), Dependency-Track and SecObserve (custom MCP, APIs already mapped), GitHub (official MCP), and AWS deployed state (awslabs MCP servers).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agent → Change (PR, not commit)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Agents propose via pull requests, never direct pushes to protected branches. The Clean-as-You-Code quality gate then runs on the agent's PR just like a human's — the same bar applies. This is the safety mechanism that makes autonomy acceptable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Outcome → Memory (feedback loop)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In an advanced Level 4 setup, each human correction should become durable, shared agent memory—not just a one-time review comment. Approved patterns, rejected approaches, architectural decisions, and remediation lessons should be captured in steering docs and connected to the enterprise knowledge base, so every agent can reuse the same organizational context. Over time, this creates a feedback loop where agents learn from reviews, align with company standards, and reduce repeated mistakes across teams and pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principles that keep this safe
&lt;/h2&gt;

&lt;p&gt;Agent integration fails when it's ungoverned. Athleon's Level 4 is bounded by:&lt;/p&gt;

&lt;p&gt;• Human-in-the-loop by default. Agents open PRs; humans merge. Autonomy is earned per workflow after it proves reliable, never granted wholesale.&lt;/p&gt;

&lt;p&gt;• The same quality gate for agents and humans. An agent PR that lowers coverage or adds issues fails the gate. No special path.&lt;/p&gt;

&lt;p&gt;• Least-privilege tooling. Each agent's allowedTools and MCP scope is the minimum for its job (e.g., sonar-fixer can read/write code and run tests, but not touch production).&lt;/p&gt;

&lt;p&gt;• No secret sprawl. KIRO_API_KEY and tool tokens live in GitHub secrets, never in agent JSON committed to the repo.&lt;/p&gt;

&lt;p&gt;• Reversibility. Every agent action is a reviewable, revertable Git change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why “company” is ready for Level 4
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Most teams that attempt agent integration are missing the foundation required to make it safe and repeatable&lt;/strong&gt;. Company is different because the core DevSecOps substrate is already in place:&lt;/p&gt;

&lt;p&gt;• Existing quality gates. Agents already have a clear definition of done: pass the quality gate and pass the tests.&lt;br&gt;
• Centralized findings. SonarQube, Dependency-Track, and SecObserve provide structured, queryable inputs that agents can act on.&lt;br&gt;
• Spec-driven execution. The team already works through .kiro/specs/, which means intent is captured precisely enough for agents to follow.&lt;br&gt;
• A working sub-agent. The existing sonar-fixer agent is the seed for a broader remediation fleet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&amp;gt; In other words, the gap is not capability. The gap is connection.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The flagship Level 4 initiative&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If the Company takes one practical step toward Level 4, it should start here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Convert sonar-fixer from a manually invoked assistant into a pipeline-triggered agent that opens review-ready pull requests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This approach reuses the gates, findings, specs, and sub-agent that already exist. It requires only one additional secret, KIRO_API_KEY, and immediately shifts the developer workflow from manual investigation to human review. It is also the cleanest proof of value for the broader agent-integrated DevSecOps roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Closing the Loop
&lt;/h2&gt;

&lt;p&gt;The second part of this journey makes one point clear: building fast with agentic AI is only half of the story. The hidden side is what happens after the prototype works—how the organization governs code quality, security, compliance, and production readiness at scale.&lt;/p&gt;

&lt;p&gt;Kiro and similar agents can accelerate development, but they become truly valuable when they are connected to mature DevSecOps foundations: quality gates, centralized findings, secure CI/CD, MCP-based context, and human review through pull requests. This is the shift from an open loop—where tools report issues and humans manually chase them—to a closed loop, where agents transform findings into review-ready fixes.&lt;/p&gt;

&lt;p&gt;For companies adopting agentic AI, the goal should not be unchecked autonomy. The goal is governed acceleration: agents operating within clear policies, limited permissions, shared enterprise context, and the same quality standards expected from human developers. In that model, humans stay in control, but their effort moves from repetitive investigation to higher-value review and decision-making.&lt;/p&gt;

&lt;p&gt;The practical next step is simple and powerful: evolve the existing sonar-fixer assistant into a pipeline-triggered agent that opens pull requests with tested, explainable remediations. That single move proves the Level 4 model while preserving safety, traceability, and human accountability.&lt;br&gt;
Closing the loop is not about replacing developers. It is about giving development teams a smarter operating model—one where DevSecOps signals become action, agent memory improves with each review, and enterprise knowledge turns isolated fixes into repeatable engineering practice.&lt;/p&gt;

&lt;p&gt;✨ Alejandro Velez, Platform Engineering Latam Lead @ GFT | AWS Ambassador&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>FAIR Data for Agentic AI</title>
      <dc:creator>N Chandra Prakash Reddy</dc:creator>
      <pubDate>Sat, 05 Sep 2026 14:41:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/fair-data-for-agentic-ai-4aaa</link>
      <guid>https://dev.to/aws-builders/fair-data-for-agentic-ai-4aaa</guid>
      <description>&lt;p&gt;I got the wonderful opportunity to attend AWS Community Day Chennai on 7th March 2026. There were a ton of great sessions throughout the day, but one specific session absolutely transformed the way I thought about the future of artificial intelligence. The topic was “FAIR Data for Agentic AI” and the speaker was Naveena Ravi.&lt;/p&gt;

&lt;p&gt;Whether you’re an experienced data engineer or just beginning your cloud adventure, knowing how to prepare your data for the next generation of AI is key.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The AI Transformation: From Predictions to Actions&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Let's be honest, keeping up with AI has felt a bit like trying to sip from a firehose the past few months. To help us understand exactly where we are headed, Naveena opened her session by breaking down the growth of artificial intelligence into three separate phases.&lt;/p&gt;

&lt;p&gt;Traditional AI was mostly about using current data to generate predictions and drive decisions. Think of it like your phone’s weather app, trying to figure out if it’s going to rain tomorrow based on previous data. Then came Generative AI that unleashed the power to generate and create brand new material from our text prompts. It’s like asking a chef to create a unique recipe for you based on your favorite ingredients.&lt;/p&gt;

&lt;p&gt;And this is when it gets interesting. We are nearing the age of Agentic AI. Agentic AI is about performing action, rather than predicting an outcome or generating a block of text. These complex algorithms can think for themselves, strategize and really do things on your behalf.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Decoding AI Agents&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;You may be asking yourself, what does it mean for an AI to be “agentic”? "A clear definition was given by Naveena: AI Agents are semi or fully independent pieces of software. They have the unique ability to reason, plan, and act to achieve certain goals. And they can also work smoothly in digital and physical situations.&lt;/p&gt;

&lt;p&gt;Think of your company’s database as a large disorganized library. A typical Generative AI model is like a speed reading assistance, if you give it a book, it will summarize the book for you. But an AI Agent is a proactive researcher. You tell it you need a report, and it scans the shelves, pulls the five most relevant-looking books, extracts the best quotes, turns it into a polished paper, and emails it to your employer.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Building Blocks of an Agent&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To enable Agentic AI to do this independent magic, four main components function in perfect balance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LLM (Large Language Model):&lt;/strong&gt; This is the main brain of the operation , allowing the system to understand human language and process complex logic .&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI Agent:&lt;/strong&gt; The orchestrator that takes the user’s aim and turns it into steps, doing the thinking and planning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RAG (Retrieval-Augmented Generation):&lt;/strong&gt; The memory system draws in relevant business facts related to the scenario so the AI does not just guess the responses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MCP (Model Context Protocol):&lt;/strong&gt; The bridge or communication layer that allows the agent to connect securely to outside tools, applications and environments.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Next Generation of Amazon SageMaker&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;These complex, interconnected systems need a very robust base for developers to build on. This is where Amazon SageMaker comes in. “The next generation of SageMaker is built to be the central hub for all of your data, analytics and AI needs.&lt;/p&gt;

&lt;p&gt;If you’re developing a startup, stitching together a dozen different tools might be an operational headache. SageMaker fulfills this need with a Unified Studio architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;A Unified Architecture&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The architectural Naveena gave was a great roadmap for today’s data teams. At the core level you have Open Lakehouse, sitting just underneath a vital layer for Data &amp;amp; AI Governance and on top of this safe base stands the Unified Studio, which divides your workflow into specialized toolsets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQL Analytics:&lt;/strong&gt; Powered by tools such as Amazon Redshift and Amazon Athena to query your data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Processing:&lt;/strong&gt; Using Amazon EMR and AWS Glue to clean and process raw data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model Development:&lt;/strong&gt; Build and train your algorithms powered by Amazon SageMaker AI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gen AI App Development:&lt;/strong&gt; Built securely using Amazon Bedrock.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Future Capabilities:&lt;/strong&gt; The ecosystem is also expanding with Streaming (Amazon MSK, Kinesis), Business Intelligence (Amazon QuickSight) and Search Analytics (Amazon OpenSearch Service) products coming soon.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Transforming Raw Data into AI Wisdom&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Here’s the thing: An AI agent is only as smart as the data you feed it. But to really enable these agents to make the right choices, your business data needs to be run through a complete, step-by-step process to be transformed into actionable information.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Data Pipeline Steps&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Ingestion:&lt;/strong&gt; The process of getting raw, unstructured data into your system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Taxonomy / Ontology:&lt;/strong&gt; Classifying and organizing the data in a way that clearly establishes linkages and hierarchies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Modelling – Schema Creation:&lt;/strong&gt; Building the actual blueprints/formats of how the data is saved.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Quality Management:&lt;/strong&gt; Cleaning the data such that the information is correct, clean and free of errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Catalog:&lt;/strong&gt; Index everything to make the data easy to find for your teams.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Knowledge Pyramid&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To give a sense of this transformation, Naveena presented a stunning pyramid that showed the journey from raw inputs to complex AI.&lt;/p&gt;

&lt;p&gt;At the very bottom base is raw &lt;strong&gt;Data&lt;/strong&gt;, the first Data Ingestion phase. One rung higher, Data Processing turns those raw inputs into useful &lt;strong&gt;Information&lt;/strong&gt;. Then at the &lt;strong&gt;Knowledge&lt;/strong&gt; level, we have the Data Catalog which organizes the data for easy discovery. And last &lt;strong&gt;Wisdom&lt;/strong&gt; where the AI Agents are. These AI Agents use all the underlying layers to act smartly.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Core: F.A.I.R. Data Principles&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;To ensure your data can safely and securely reach that peak “Wisdom” stage, it must strictly follow the F.A.I.R. Data Principles. Is that you? F.A.I.R. stands for Findable, Accessible, Interoperable and Reusable.&lt;/p&gt;

&lt;p&gt;Let's go over what this means for your data infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Findable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For data to be useful, it needs to be easily discoverable by human workers and computer systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Organizations need to have the right governance to tightly control metadata.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The metadata must be significant, very detailed and continuously persistent across the whole business.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider this as an online e-commerce store. If a new pair of shoes is not labeled with the correct category, color and size description, no customer (or search engine algorithm) will be able to find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Accessible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When data is found it must be accessible in a safe manner, with no extra barriers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;To keep security, the actual data should be strictly accessible to authorized individuals only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But the descriptive metadata needs to be available to relevant people and AI agents so they know what’s there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The protocol for accessing this information must be open or easily recognized for conventional authentication and authorization procedures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Crucially, metadata must be available even if the underlying data is removed or is no longer available.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Interoperable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Data should not be separate, disconnected silos.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Metadata shall be in standardized, FAIR certified formats.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This makes it easy to identify the data across numerous distinct systems and different AIs Agents.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The data itself must be able to easily cooperate with apps and workflows, allowing easy storage, processing and analysis.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simply said, your data should have a common language. If your marketing software speaks French and your sales software speaks Japanese, your AI Agent won’t be able to help you. Compatibility means everyone speaks the same technological language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reusable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And finally, solid data is an asset that should continue to provide value over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Metadata must be extremely reusable so engineering teams may design complex, interconnected systems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It should describe both the metadata and the actual data in a thorough and good way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;At this level of description, the information is easily copied or merged for fresh new use cases down the line.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Key Takeaways&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The secret to building mind-blowingly intelligent, autonomous AI agents isn't picking the latest language model, it's really a data organization challenge.&lt;/p&gt;

&lt;p&gt;At the end of the session, Naveena raised a vital question to the audience, "Is your DATA Agent Ready?" To be able to answer yes, with confidence, organizations need to heavily focus on these essential pillars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Quality is non-negotiable:&lt;/strong&gt; unavoidably bad data leads to unavoidably faulty, and even dangerous, agent decisions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strict Data Governance:&lt;/strong&gt; You want to make sure security, compliance and adequate control of access are all locked down entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear Data Lineage:&lt;/strong&gt; You must know precisely where your data started and how it has evolved throughout its existence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embrace F.A.I.R. Principles:&lt;/strong&gt; Only Findable, Accessible, Interoperable, and Reusable data can bridge the gap between fundamental information and actual AI wisdom.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;After all, transitioning from standard AI to Agentic AI is a tremendous technological leap. We’re not simply having computers predict the future or write words anymore; we’re trusting them to develop plans and perform real-world activities for us.&lt;/p&gt;

&lt;p&gt;To summarize, before unleashing autonomous AI to tackle your complicated business difficulties, you need to make sure your data house is clean, structured, and properly managed. If you’re leading a data team today, I highly recommend checking out the unified design of Amazon SageMaker, and start evaluating your own pipelines to see just how F.A.I.R. your data really is.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;About the Author&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;As an &lt;strong&gt;AWS Community Builder&lt;/strong&gt;, I enjoy sharing the things I've learned through my own experiences and events, and I like to help others on their path. If you found this helpful or have any questions, don't hesitate to get in touch! 🚀&lt;/p&gt;

&lt;p&gt;🔗 Connect with me on &lt;a href="https://www.linkedin.com/in/chandra-prakash-reddy/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;References&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Event:&lt;/strong&gt; AWS Community Day Chennai&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topic:&lt;/strong&gt; FAIR Data for Agentic AI&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date:&lt;/strong&gt; March 7, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Also Published On&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://builder.aws.com/content/3Iudi3L8vn7EJSaPa3byVjK5VDu/fair-data-for-agentic-ai" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopstour.hashnode.dev/fair-data-for-agentic-ai" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>agents</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>From Data Lake to Lakehouse: Building Modern Analytics Platforms on AWS with S3 Tables &amp; AWS Glue</title>
      <dc:creator>N Chandra Prakash Reddy</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:05:49 +0000</pubDate>
      <link>https://dev.to/aws-builders/from-data-lake-to-lakehouse-building-modern-analytics-platforms-on-aws-with-s3-tables-aws-glue-4fde</link>
      <guid>https://dev.to/aws-builders/from-data-lake-to-lakehouse-building-modern-analytics-platforms-on-aws-with-s3-tables-aws-glue-4fde</guid>
      <description>&lt;p&gt;Okay, we will be honest – data architecture is usually one of those topics that makes your head spin. I was there at AWS Community Day Chennai on 07-Mar-2026. There were a lot of good talks on many kinds of cloud technologies, but one speaker in particular caught my eye.&lt;/p&gt;

&lt;p&gt;Then we had Vishali Sakthivel and Vikneshwara RB take the stage to discuss about transitioning from Data Lakes to Lakehouses with Amazon S3 Tables and AWS Glue. If you've ever wrestled with messy data pipelines, this workshop felt like a breath of fresh air. I want to break out exactly what I learnt.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Headache of Traditional Data Architectures&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Sound familiar? We did data analytics with on-prem data warehouses for the longest period. This old system has some significant baggage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We had to work with strict concepts that were really hard to alter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We were trapped with ETL heavy pipelines that had to be babysat constantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling was brutally expensive and vendor lock-in was a significant risk.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even as we migrated to the cloud there were still serious engineering problems. Data engineers have always struggled with the “small files problem"  the presence of thousands of little files would significantly impact query performance.&lt;/p&gt;

&lt;p&gt;Imagine reading a book where every sentence is printed on a separate piece of paper and spread out throughout a room. That’s how your database feels dealing with the little files problem. We also suffered schema evolution issues, slow analytical queries, difficult pipeline maintenance, late arriving data and duplicate records.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Enter the Lakehouse and Apache Iceberg&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Here is when things become interesting. The industry responded to these problems by moving to the “Lakehouse” architecture. A Lakehouse can be thought of as the best of both worlds. It combines the vast storage scale of a Data Lake and the organized reliability of a Data Warehouse.&lt;/p&gt;

&lt;p&gt;Apache Iceberg is the core of this transition. Iceberg is an open table format for data streaming and Lakehouses. You might think of it as a very efficient index for your data. It lets different compute engines like Apache Flink, Spark, Snowflake and Athena read the exact same data without duplicating it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Maintenance Trap&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;But running Apache Iceberg yourself on AWS isn’t a picnic. The speakers raised several serious challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operational Overhead:&lt;/strong&gt; You need to keep a close watch on the system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metadata Explosion:&lt;/strong&gt; Tracking data changes creates huge metadata files which bog things down.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual Maintenance Trap:&lt;/strong&gt; Engineers run compaction jobs for hours at a time to maintain things healthy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Catalog Consistency:&lt;/strong&gt; It is hard to keep your metadata catalog fully in sync.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;S3 Tables to the Rescue&lt;/strong&gt;
&lt;/h1&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%2Fziohy227alj24vmjivxt.jpeg" 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%2Fziohy227alj24vmjivxt.jpeg" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The issue is: Amazon S3 Tables takes care of all that messy operational overhead. S3 Tables offers fully managed Iceberg tables with automatic table maintenance. Simply put, AWS conducts the heavy lifting behind the scenes, such as cleaning up small files and optimizing metadata, so you don't have to.&lt;/p&gt;

&lt;p&gt;The architecture consists of few building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Amazon S3:&lt;/strong&gt; Storage Buckets Table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS Glue:&lt;/strong&gt; For data transformations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Amazon Athena:&lt;/strong&gt; For SQL-based analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;QuickSight (BI Reports):&lt;/strong&gt; For visual dashboards.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Structuring Data: The Medallion Architecture&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The workshop was designed to function seamlessly for that hence the talk was on the Medallion Architecture, best practice in arranging data into three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bronze Layer:&lt;/strong&gt; This is for raw ingestion and history . It accepts CSV, JSON, and TXT files as they are. This was the raw orders and raw customers tables in their retail use case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silver Layer:&lt;/strong&gt; Here the data is filtered, cleansed, and supplemented. The dataset gets cleaned up and silver_orders and silver_customers are considerably more readable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gold Layer:&lt;/strong&gt; This is the aggregate layer of the business level. It has facts and dimensions like gold_fact_orders and gold_customer_metrics, which are suited for BI reporting and Machine Learning.&lt;/p&gt;&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%2Fsplod0y2x9q7hiimr07r.jpeg" 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%2Fsplod0y2x9q7hiimr07r.jpeg" alt=" " width="799" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They showed a retail analytics use case where data was moving from an S3 bucket to ingestion jobs to Bronze S3 table, transformed through Glue to Silver and transformed again to Gold, and then pushed to QuickSight and SageMaker. Inside an S3 Table bucket in the AWS dashboard these managed tables were plainly visible, grouped neatly.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Getting Data In: Choose Your Weapon&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;You may be asking how we actually get data into these S3 tables? The speakers broke down three good intake approaches using user profile and data size. The best thing? They showed us the code itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Python (PyIceberg &amp;amp; PyArrow) for Small/Medium Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are running lightweight data pipelines or single-threaded python programs, PyIceberg + PyArrow is your best friend.&lt;/p&gt;

&lt;p&gt;First, you’ll need to connect to the S3 Tables catalog. Think of the catalog as the master index that points Python directly to where your data exists. Notice the usage of AWS SigV4 in the settings, which ensures that your connection is safely authenticated with your normal AWS credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from pyiceberg.catalog import load_catalog

catalog = load_catalog(
    "s3tables_catalog",
    **{
        "type": "rest",
        "uri": f"https://s3tables.{region}.amazonaws.com/iceberg",
        "warehouse": table_bucket_arn,
        "rest.sigv4-enabled": "true",
        "rest.signing-name": "s3tables",
        "rest.signing-region": region,
    }
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we connect we are able to construct a namespace (think of it as a folder for our tables) and specify our schema using PyArrow . Imagine a schema as the column headers of an Excel spreadsheet, informing the database what kind of data to expect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import pyarrow as pa

# Create a namespace
catalog.create_namespace("demo_ns")

# Define exactly what our data looks like
schema = pa.schema([
    pa.field("order_id", pa.int32()),
    pa.field("customer_name", pa.string()),
    pa.field("product", pa.string()),
    pa.field("quantity", pa.int32()),
    pa.field("price", pa.float64()),
    pa.field("status", pa.string()),
])

# Create the empty table
table = catalog.create_table("demo_ns.orders", schema=schema)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s insert some real data into our table and get it back. The very fascinating thing here is the "predictive pushdown” of the filtered scan. In layman's terms, this means that we are saying to the database, 'just give me the rows where the status is "shipped"' and so we avoid having to download large quantities of irrelevant data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create some sample data
rows = [
    {"order_id": 1, "customer_name": "Alice", "product": "Widget A", "quantity": 10, "price": 29.99, "status": "shipped"},
    {"order_id": 2, "customer_name": "Bob", "product": "Widget B", "quantity": 5, "price": 49.99, "status": "pending"},
    {"order_id": 3, "customer_name": "Charlie", "product": "Widget C", "quantity": 2, "price": 99.99, "status": "shipped"},
]

# Write data to the table
arrow_table = pa.Table.from_pylist(rows, schema=schema)
table.append(arrow_table)

# Filtered scan: Only pull data where status is "shipped"
from pyiceberg.expressions import EqualTo
df_shipped = table.scan(
    row_filter=EqualTo("status", "shipped")
).to_pandas()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Spark (AWS Glue) for Big Data &amp;amp; ETL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to do large scale heavy duty data transformation then AWS Glue running on Spark is your go-to service. You just inject the Iceberg extensions into your Spark Session and no hard workarounds are needed to make Spark connect to S3 Tables.&lt;/p&gt;

&lt;p&gt;This is what that Glue Job configuration looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ----------------------------------------------------
# Spark Session Configuration for S3 Tables
# ----------------------------------------------------
spark = (
    SparkSession.builder
    .appName(job_name)
    .config(
        "spark.sql.extensions",
        "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions",
    )
    .config(
        f"spark.sql.catalog.{catalog_name}",
        "org.apache.iceberg.spark.SparkCatalog",
    )
    .config(
        f"spark.sql.catalog.{catalog_name}.catalog-impl",
        "software.amazon.s3tables.iceberg.S3TablesCatalog",
    )
    .config(
        f"spark.sql.catalog.{catalog_name}.warehouse",
        bucket_arn,
    )
    .getOrCreate()
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. SQL (Amazon Athena) for Analysts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are an analyst who prefers SQL, you can work with S3 Tables directly from the Athena console.&lt;/p&gt;

&lt;p&gt;For example, running the famous “Time Travel” functionality we talked about above is as simple as adding FOR VERSION AS OF at the end of a normal SQL query. Here’s a sample from the session to show how you may query a historical snapshot of the data:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT * FROM "daily_sales" FOR VERSION AS OF 2246846639951314761;&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The "Wow" Moments: ACID, Time Travel, and Schema Evolution&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;To be fair there are a lot of tools that can shift data around. But S3 Tables adds real database like features to object storage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ACID Transactions:&lt;/strong&gt; Now you can execute &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; operations straight in Athena. If a consumer deletes their account, you perform a normal SQL delete command on your data lake.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time Travel &amp;amp; Snapshot Isolation:&lt;/strong&gt; Just think about finding out you've erased the wrong records by mistake. S3 Tables lets you query data as it was in the past, literally, with a single SQL command: &lt;code&gt;SELECT * FROM table FOR VERSION AS OF [snapshot_id]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schema Evolution:&lt;/strong&gt; Business needs are always changing. To add a new column “City” to your database, just run an &lt;code&gt;ALTER TABLE&lt;/code&gt; command. It updates instantly without having to re-write the full history dataset.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Turning Data into Business Insights&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;After all, data is only valuable if it answers business questions. The speakers presented a highly optimized query layer on top of the Gold layer with Amazon Athena. Athena uses partition pruning and is highly optimized for Iceberg, so you only pay for the exact data you scan.&lt;/p&gt;

&lt;p&gt;They ran some interesting analytical queries from the actual world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monthly Revenue Trend:&lt;/strong&gt; Aggregating total orders and revenue by month.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customer Lifetime Value &amp;amp; Segmentation:&lt;/strong&gt; Classifying Customers to Platinum, Gold, Silver and Bronze levels depending on their total spend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repeat vs. One-Time Buyers:&lt;/strong&gt; Behavior analysis to determine how many customers return vs. buy once.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These SQL queries were then wonderfully presented using Amazon QuickSight dashboards to generate pie charts and bar graphs that business stakeholders could truly utilize.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Future is Here: Claude + MCP Server&lt;/strong&gt;
&lt;/h1&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%2Fu69le963twgf973moaqp.jpeg" 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%2Fu69le963twgf973moaqp.jpeg" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Long story short, writing SQL can still be a bottleneck for non-technical people. The speakers thrilled the audience by displaying an integration with Anthropic’s Claude AI with an MCP (Model Context Protocol) server.&lt;/p&gt;

&lt;p&gt;They set up a local MCP server called &lt;code&gt;s3tablesagent&lt;/code&gt; which enabled Claude to securely access the S3 Tables data warehouse. In the chat they just wrote: "What are the top 10 customers by total revenue and what is their average order value?".&lt;/p&gt;

&lt;p&gt;Claude got the intent, queried the Gold tables in the background, and sent back a neatly prepared markdown table with significant business insights, such as “Customer 92 combines both strategies well - fewer orders but a strong average order value”. It was like having a senior data analyst sitting inside the chat window!&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Key Takeaways&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Here’s a quick summary of what you need to remember based on the summary given at the end of the session:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Best of Both Worlds:&lt;/strong&gt; The Lakehouse architecture (Amazon S3 Tables and Apache Iceberg) combines the huge volume of a data lake with the dependability and performance of a traditional data warehouse.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero Maintenance:&lt;/strong&gt; Iceberg administration is simple, because Amazon S3 Tables takes care of metadata, compaction and table optimization behind the scenes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Massive Scale without Servers:&lt;/strong&gt; AWS Glue lets you develop distributed, large-scale ETL pipelines without ever handling the underlying infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serverless SQL:&lt;/strong&gt; Amazon Athena enables data analysts to query big datasets immediately using conventional SQL without having to manage clusters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better Organization:&lt;/strong&gt; By organizing your data, the Medallion Architecture (Bronze, Silver, and Gold layers) will increase your data reliability, management, and ability to prepare for analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Faster Decisions:&lt;/strong&gt; You can also build Amazon Athena and QuickSight on top of handpicked datasets, to get powerful business insights and dashboards, much faster.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The conclusion is as follows: The new data architecture is evolving rapidly and AWS makes it very accessible. This session was a masterclass on updating your analytics platforms.&lt;/p&gt;

&lt;p&gt;Vishali and Vikneshwara performed an amazing job solving complicated, headache producing data engineering difficulties and giving a clean, automated and highly scalable approach. Are you exhausted of wrestling with complicated data pipelines, strict schemas, and small file problems? Amazon S3 Tables might be the tool you have been waiting for.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;About the Author&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;As an &lt;strong&gt;AWS Community Builder&lt;/strong&gt;, I enjoy sharing the things I've learned through my own experiences and events, and I like to help others on their path. If you found this helpful or have any questions, don't hesitate to get in touch! 🚀&lt;/p&gt;

&lt;p&gt;🔗 Connect with me on &lt;a href="https://www.linkedin.com/in/chandra-prakash-reddy/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;References&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Event:&lt;/strong&gt; AWS Community Day Chennai&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topic:&lt;/strong&gt; From Data Lake to Lakehouse: Building Modern Analytics Platforms on AWS with S3 Tables &amp;amp; AWS Glue&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date:&lt;/strong&gt; March 7, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Also Published On&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://builder.aws.com/content/3IuHPodR1xhHrA8LVjKrW5DT2gc/from-data-lake-to-lakehouse-building-modern-analytics-platforms-on-aws-with-s3-tables-and-aws-glue" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopstour.hashnode.dev/from-data-lake-to-lakehouse-building-modern-analytics-platforms-on-aws-with-s3-tables-aws-glue" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>analytics</category>
      <category>sql</category>
    </item>
    <item>
      <title>I Tried Nx Plugin for AWS, Here's Why I'm Sold</title>
      <dc:creator>Steven Leung</dc:creator>
      <pubDate>Sat, 05 Sep 2026 09:40:51 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-tried-nx-plugin-for-aws-heres-why-im-sold-47ce</link>
      <guid>https://dev.to/aws-builders/i-tried-nx-plugin-for-aws-heres-why-im-sold-47ce</guid>
      <description>&lt;p&gt;Who hasn't built a full-stack app on AWS before, we all know the drill. You need an API (usually Lambda with API Gateway), a frontend, some authentication (Cognito) wired up, and IaC (CDK) to help deploy the app. On their own, none of that is hard, but wiring it up all together, especially in a team where every developer has their own style, always takes an amount of time before you even get to write a single line for the business logic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://awslabs.github.io/nx-plugin-for-aws/" rel="noopener noreferrer"&gt;Nx Plugin for AWS&lt;/a&gt; (&lt;code&gt;@aws/nx-plugin&lt;/code&gt;), an AWS Labs open source project, tackles that problem with code generators built on top of &lt;a href="https://nx.dev" rel="noopener noreferrer"&gt;Nx&lt;/a&gt;. Instead of once again writing out boilerplate for every new service or website, you can just run a generator (CLI), answer a few questions, and get production-ready application code plus the CDK or Terraform to deploy it.&lt;/p&gt;

&lt;p&gt;This article introduces what the plugin does, the core concepts, and how to scaffold a complete full-stack app. What sets Nx Plugin apart from just another scaffolding tool?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on versioning:&lt;/strong&gt; at time of this article the plugin was pre-1.0, currently working through a &lt;code&gt;1.0.0-rc.x&lt;/code&gt; release candidate series with regular updates. Commands and generator names below were accurate at time of writing, check &lt;code&gt;npm view @aws/nx-plugin version&lt;/code&gt; before you start, given the pace, it's likely to have moved on since this article published.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A quick primer on Nx
&lt;/h3&gt;

&lt;p&gt;For the uninitiated &lt;a href="https://nx.dev" rel="noopener noreferrer"&gt;Nx&lt;/a&gt; is a toolkit specifically for monorepos. Two things define Nx:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A dependency graph across projects.&lt;/strong&gt; Nx knows how the individual parts of your projects relate to each other (which website depends on which API, which library is shared where), and it uses that graph to only build, test, or lint the things actually affected by a change, with results cached so recurring CI/CD runs should be fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generators.&lt;/strong&gt; Nx has a plugin system where a package can register generators, scripts that scaffold or modify code in your workspace, invoked via the &lt;code&gt;nx g&lt;/code&gt; (or &lt;code&gt;nx generate&lt;/code&gt;) command. This is the bit &lt;code&gt;@aws/nx-plugin&lt;/code&gt; builds on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nx is not AWS-specific, rather AWS adopted and built their plugin on top of Nx. &lt;code&gt;@aws/nx-plugin&lt;/code&gt; doesn't replace any of that, it's a collection of generators in your toolbag that plug into your codebase.&lt;br&gt;
Ideally suited for greenfield projects, with a bit of uplift can work with existing codebases.&lt;/p&gt;

&lt;p&gt;So when you run &lt;code&gt;pnpm nx g @aws/nx-plugin:ts#api&lt;/code&gt;, you're using Nx's own generator mechanism, just pointed at generators AWS Labs has written and opinionated toward AWS services. That also means everything else Nx gives you for free, the dependency graph, caching, task running, applies to the projects the plugin generates without any extra setup.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it actually gives you
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@aws/nx-plugin&lt;/code&gt; is a collection of generators for Agentic, API, Lambda, Data Storage, Website (inc Auth), IaC on AWS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end by default.&lt;/strong&gt; Generators produce both application code and the accompanying infrastructure (CDK or Terraform), so you're not left in the lurch wiring up deployment separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component-based.&lt;/strong&gt; You add pieces as you need them, an API here, a website there, rather than committing to one overwhelming opinionated template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your code, not a black box.&lt;/strong&gt; Everything generated is regular source in your repo. If you don't like something, edit it. There's no hidden framework layer to fight later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-safe by design.&lt;/strong&gt; Where it makes sense, type safety flows through from backend to frontend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first development.&lt;/strong&gt; One of the strengths of Nx is you can develop and iterate against pure local dev and testing, alternatively your local code can also be pointed towards your deployed AWS resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worthy callouts
&lt;/h4&gt;

&lt;p&gt;Everything sits inside your monorepo, thanks to Nx Plugin for AWS, you can use it without knowing about Nx which means you get all the benefits of Nx's dependency graph, caching, and task running without the learning curve for your project.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@aws/nx-plugin&lt;/code&gt; is a build-time tool, not a runtime dependency. Once a generator has performed its job, the plugin is no longer relevant.&lt;/p&gt;

&lt;p&gt;You can read every file it produced, commit it, and change it however you want afterwards.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There’s no proprietary learning curve or configuration to wrestle with when you need to do something the generator didn’t anticipate. You take what was given to you and edit the generated code directly, the same way you would edit any code you wrote yourself.&lt;/li&gt;
&lt;li&gt;No new layers of abstraction. Generators emit plain code. There’s no opinionated wrapper API or unknown black box getting between you and frontend, backend, CDK or any of the other tools; what you see is exactly what you get.&lt;/li&gt;
&lt;li&gt;The plugin isn’t a dependency. It generates code and steps out of the way. Your application doesn’t import or rely on &lt;code&gt;@aws/nx-plugin&lt;/code&gt; at runtime, so you are never locked in, and there's no unsightly evidence of it in your code repository either. If you don’t like something in the generated code, you are free to modify it. Generators exist to get you started, not to be a prison with a lock and key.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The generators
&lt;/h3&gt;

&lt;p&gt;Here's the current set of generators available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generator&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TypeScript library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TypeScript API (tRPC or Smithy) with API Gateway, Lambda, and Powertools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#rdb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Relational database support via Aurora RDS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#website&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;React app (Vite)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#website#auth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adds Cognito auth to a website&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#infra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AWS CDK infrastructure project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#lambda-function&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TypeScript Lambda with type-safe event sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#mcp-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MCP server (TypeScript)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts#agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strands Agent (TypeScript)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py#project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python project (managed with uv)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py#api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python API (FastAPI) with API Gateway, Lambda, and Powertools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py#lambda-function&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python Lambda with type-safe event sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py#mcp-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MCP server (Python)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py#agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strands Agent (Python)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wires projects together, e.g. connecting a frontend to a backend API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform#project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Terraform project, for teams that prefer Terraform over CDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;license&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manages LICENSE files and source headers across the workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a slice, not the full list, the plugin also ships generators for DynamoDB, relational databases in Python, docs sites, and agent-specific infra (AgentCore gateway/harness) among others. See the &lt;a href="https://awslabs.github.io/nx-plugin-for-aws/en/get_started/building-with-ai/" rel="noopener noreferrer"&gt;full generators list&lt;/a&gt; in the docs sidebar for everything currently available.&lt;/p&gt;
&lt;h3&gt;
  
  
  The connection generator is the real workhorse
&lt;/h3&gt;

&lt;p&gt;Sure adding individual components is great, however getting these individual components to play nicely with each other is another.&lt;br&gt;
So my favourite generator: &lt;code&gt;connection&lt;/code&gt; is just magic.&lt;/p&gt;

&lt;p&gt;It transforms your project in meaningful ways that make sense, i.e connecting a React site to a API actually generates the wiring between the two, rather than leaving you to do it by hand.&lt;/p&gt;

&lt;p&gt;A few of the pairings it supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;React website to an API backend. It generates a type-safe client, sets up the provider your React app needs, and configures auth (IAM or Cognito) so calls are signed correctly. If your API's types change later, your frontend calls stay in sync without you touching the wiring again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Frontend to an Agent. Same idea as the API case, your website gets a typed way to talk to a deployed agent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Agent to an MCP server. Agents often need tools, and MCP servers are how the plugin exposes them. connection handles the IAM-authenticated link between an agent and an MCP server so the agent can call its tools without you writing the boilerplate auth and transport code by hand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Agent to Agent. Connect two agents together using the A2A protocol.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API (or Agents) to Data. Data meaning Relational Database or Dynamo DB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Service to service, more generally. Anywhere you've got a source project that needs to call a target project's API, connection is the generator that sets up the client, the auth, and the local dev configuration in one go.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For sake of brevity, too many permutations so better to check out list of various &lt;a href="https://awslabs.github.io/nx-plugin-for-aws/en/guides/connection/" rel="noopener noreferrer"&gt;supported connections&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before you start, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Node 22 or later&lt;/li&gt;
&lt;li&gt;A package manager: pnpm 10+, yarn 4+, bun 1+, or npm 10+&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; 0.5.29 or later, plus Python 3.12 (&lt;code&gt;uv python install 3.12.0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;AWS CLI configured, profile/SSO credentials for the AWS account you're deploying to&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; (Docker Desktop on Mac/Windows, the Engine directly on Linux), or &lt;a href="https://runfinch.com/" rel="noopener noreferrer"&gt;Finch&lt;/a&gt; 1.6.0+, required for some generators&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Optional IDE extensions
&lt;/h3&gt;

&lt;p&gt;You can get a UI over the generators for the major IDEs, which is useful for those devs like me who may not prefer the terminal experience. Both editors' extensions are thin wrappers around the same underlying Nx Language Server, so functionality is on par.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;JetBrains &lt;a href="https://plugins.jetbrains.com/plugin/21060-nx-console" rel="noopener noreferrer"&gt;official Nx Console plugin for JetBrains IDEs&lt;/a&gt; (WebStorm, IntelliJ IDEA Ultimate, and others), built and maintained by the Nx team rather than a community fork, with the same generator UI and project graph view.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VS Code, install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console" rel="noopener noreferrer"&gt;Nx Console extension&lt;/a&gt; too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Scaffolding a project
&lt;/h3&gt;

&lt;p&gt;Here's a typical flow: an API, a AI agent (Strands), a React website with Cognito auth, connected together, with CDK infrastructure to deploy it.&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="c"&gt;# Create a new workspace&lt;/span&gt;
pnpm create @aws/nx-workspace my-project
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project

&lt;span class="c"&gt;# Add an API (tRPC)&lt;/span&gt;
pnpm nx g @aws/nx-plugin:ts#api &lt;span class="nt"&gt;--framework&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trpc

&lt;span class="c"&gt;# Add a Strands AI agent (Python)&lt;/span&gt;
pnpm nx g @aws/nx-plugin:py#agent

&lt;span class="c"&gt;# Add a React website&lt;/span&gt;
pnpm nx g @aws/nx-plugin:ts#website &lt;span class="nt"&gt;--framework&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;react

&lt;span class="c"&gt;# Add authentication to your website&lt;/span&gt;
pnpm nx g @aws/nx-plugin:ts#website#auth

&lt;span class="c"&gt;# Connect your website to your API and agent&lt;/span&gt;
pnpm nx g @aws/nx-plugin:connection

&lt;span class="c"&gt;# Add CDK infrastructure to deploy it all (or choose Terraform)&lt;/span&gt;
pnpm nx g @aws/nx-plugin:ts#infra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that's done, you can run everything locally with hot reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm nx run-many &lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;serve &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nifty that both your website and API launch with one CLI command, the website will be available at &lt;code&gt;http://localhost:4200&lt;/code&gt;, and changes to either the website or the API will hot-reload as you work. When you're ready to deploy, the generated CDK (or Terraform) project handles pushing it all to AWS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build and deploy
&lt;/h3&gt;

&lt;p&gt;Once you've defined your cloud resources (e.g. instantiating the generated constructs in CDK, or wiring up the equivalent Terraform modules), building and deploying is a handful of commands:&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="c"&gt;# Build everything&lt;/span&gt;
pnpm build

&lt;span class="c"&gt;# Fix any lint errors automatically&lt;/span&gt;
pnpm lint

&lt;span class="c"&gt;# If the build reports "The workspace is out of sync", sync it&lt;/span&gt;
pnpm nx &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then bootstrap and deploy your infrastructure project (named &lt;code&gt;infra&lt;/code&gt; by default, swap in whatever you actually called yours):&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="c"&gt;# Bootstrap (one-time per account/region)&lt;/span&gt;
pnpm nx bootstrap infra

&lt;span class="c"&gt;# Deploy, CDK&lt;/span&gt;
pnpm nx deploy-sandbox infra

&lt;span class="c"&gt;# Deploy, Terraform (runs plan then apply)&lt;/span&gt;
pnpm nx apply infra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once deployed, pull down the runtime config so your local website points at the real, deployed API and auth resources instead of local ones (again, swap &lt;code&gt;demo-website&lt;/code&gt; for your website project's actual name):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm nx load-runtime-config demo-website
pnpm nx serve demo-website
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Better together with AI assistants
&lt;/h3&gt;

&lt;p&gt;In this day and age, a tech article would be incomplete if I didn't work AI into the conversation. AI assistants and Nx Plugin are individually great on their own, but together they will give the developer a whole new way of working.&lt;/p&gt;

&lt;p&gt;Nx Plugin also has an MCP server available. Workspaces created with the plugin's preset already include automatic project-level MCP config for six common agents: Claude Code, Cursor, Kiro, Gemini CLI, GitHub Copilot, and Codex. Any of those work inside the workspace with no extra setup, and if you're using another MCP-compatible assistant, you can still skip typing generator commands entirely with a bit of manual config.&lt;/p&gt;

&lt;p&gt;Then just describe what you want:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use the Nx Plugin for AWS to build a full-stack app with a React website, an API, Cognito auth, and CDK infrastructure."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The assistant calls the generators for you through MCP, which is a great way to learn the available options if you have not already familiarised yourself with the documentation, i.e. generator flags available.&lt;/p&gt;

&lt;h4&gt;
  
  
  Configure your AI assistant
&lt;/h4&gt;

&lt;p&gt;Add it with whichever of these matches your setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; installs it as a plugin:

&lt;code&gt;/plugin marketplace add awslabs/nx-plugin-for-aws&lt;/code&gt;

then

&lt;code&gt;/plugin install nx-plugin-for-aws@nx-plugin-for-aws&lt;/code&gt;

.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro&lt;/strong&gt; gets a dedicated Power (bundling the MCP server with steering docs and workflow guides) via the Kiro Powers panel, or the Kiro CLI via &lt;code&gt;kiro-cli mcp add&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most MCP-compatible assistants use this JSON in a configuration file. Add the following:&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;"mcpServers"&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;"nx-plugin-for-aws"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&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/nx-plugin-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; and &lt;strong&gt;Codex&lt;/strong&gt; add the same MCP server entry to their respective config files (&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; or &lt;code&gt;~/.codex/config.toml&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cline&lt;/strong&gt;, &lt;strong&gt;Roo Code&lt;/strong&gt;, and other MCP-compatible assistants can support the above json.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where to from here
&lt;/h3&gt;

&lt;p&gt;I tried out the &lt;a href="https://awslabs.github.io/nx-plugin-for-aws/en/get_started/quick-start/" rel="noopener noreferrer"&gt;Quick Start guide&lt;/a&gt; from start to finish, it covers the concepts in this article in more depth, and the &lt;a href="https://awslabs.github.io/nx-plugin-for-aws/en/get_started/tutorials/dungeon-game/overview/" rel="noopener noreferrer"&gt;Dungeon Adventure tutorial&lt;/a&gt; is one of the best guided walkthroughs you'll see, with most of the generators used together in a full working example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts
&lt;/h3&gt;

&lt;p&gt;Pre-1.0 status (see the note near the top) hasn't put me off, honestly it is already a solid product and provides a great developer experience.&lt;/p&gt;

&lt;p&gt;If you're already building on AWS, I definitely say it is worth a try. Even just using only one generator earns its keep, say &lt;code&gt;connection&lt;/code&gt; to wire up an existing frontend and API, can save you a fair amount of repetitive boilerplate.&lt;/p&gt;

&lt;p&gt;If you get stuck or want to chat with other people using it, the maintainers hang out in the &lt;code&gt;#nx-plugin-for-aws&lt;/code&gt; channel on the &lt;a href="https://cdk-dev.slack.com/archives/C0AG11EUHM4" rel="noopener noreferrer"&gt;CDK Slack&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Documentation: &lt;a href="https://awslabs.github.io/nx-plugin-for-aws" rel="noopener noreferrer"&gt;https://awslabs.github.io/nx-plugin-for-aws&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/awslabs/nx-plugin-for-aws" rel="noopener noreferrer"&gt;https://github.com/awslabs/nx-plugin-for-aws&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>nx</category>
      <category>serverless</category>
      <category>opensource</category>
    </item>
    <item>
      <title>EKS Auto Mode: correr Kubernetes sin andar peleando con los nodos</title>
      <dc:creator>Bianca Torres</dc:creator>
      <pubDate>Fri, 04 Sep 2026 18:54:26 +0000</pubDate>
      <link>https://dev.to/aws-builders/eks-auto-mode-correr-kubernetes-sin-andar-peleando-con-los-nodos-5954</link>
      <guid>https://dev.to/aws-builders/eks-auto-mode-correr-kubernetes-sin-andar-peleando-con-los-nodos-5954</guid>
      <description>&lt;p&gt;Si algo aprendí gestionando EKS en producción, es que mantener los nodos se lleva más tiempo del que uno imagina. Managed node groups, Karpenter, el EBS CSI driver, actualizar AMIs, y el clásico "¿por qué este pod lleva 10 minutos en Pending?" a la hora menos pensada. No es lo más entretenido del rol, pero es parte de lo que nos toca hacer para mantener un cluster sano en producción.&lt;/p&gt;

&lt;p&gt;Por eso, cuando probé EKS Auto Mode, me hice esta pregunta: "si dejo que AWS haga todo esto por mí, ¿qué gano y qué pierdo?". Este artículo es esa respuesta, con un laboratorio que puedes seguir tú mismo en menos de 30 minutos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mantener nodos es casi un segundo trabajo
&lt;/h2&gt;

&lt;p&gt;Antes de vender la solución, hablemos del problema de verdad. En un EKS "clásico", tú te encargas de un montón de cosas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crear y dimensionar los node groups&lt;/strong&gt;. ¿m5.large? ¿c6i.xlarge? ¿Spot u On-Demand? Cada decisión te la cobra el futuro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instalar y afinar un autoscaler&lt;/strong&gt;. Cluster Autoscaler o Karpenter. Los dos piden configuración, permisos IAM y mantenimiento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuidar los add-ons&lt;/strong&gt;. CoreDNS, kube-proxy, EBS CSI, VPC CNI... instalarlos, versionarlos, actualizarlos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parchear las AMIs&lt;/strong&gt;. Cada vez que aparece una vulnerabilidad de seguridad en el sistema operativo del nodo (CVE), es nuestro trabajo subsanarlo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggear el scheduling&lt;/strong&gt;. Ese &lt;code&gt;0/3 nodes are available: Insufficient memory&lt;/code&gt; que aparece justo cuando ya te ibas a desconectar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo irónico es que nada de eso hace tu producto mejor. Es puro "impuesto de plataforma". Trabajo que haces para que Kubernetes exista, no para que tu app sea mejor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entonces, ¿Qué es EKS Auto Mode?
&lt;/h2&gt;

&lt;p&gt;AWS se hace cargo del cómputo, el almacenamiento y el networking del cluster por ti. Por dentro usa Karpenter y nodos con Bottlerocket, pero todo eso queda tras bambalinas. Tú ya no administras node groups ni instalas add-ons uno por uno.&lt;/p&gt;

&lt;p&gt;Tú declaras tus apps, AWS decide y levanta la infraestructura para correrlas. Los nodos aparecen cuando tus pods los necesitan, y se van cuando ya no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab: de cero a app corriendo, con nodos que aparecen solos
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lo que necesitas:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI v2 configurado (aws configure)&lt;/li&gt;
&lt;li&gt;eksctl v0.195.0 o más&lt;/li&gt;
&lt;li&gt;kubectl&lt;/li&gt;
&lt;li&gt;Permisos IAM sobre EC2, EKS, IAM y VPC&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Paso 1: Crear el cluster con Auto Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eksctl create cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; demo-automode &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-auto-mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese único comando te crea el cluster, la VPC, las subredes, activa Auto Mode y deja los add-ons esenciales gestionados por AWS. Tarda aproximadamente unos 15 minutos.&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%2F9v3u1tatl83kisvak02o.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%2F9v3u1tatl83kisvak02o.png" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h3&gt;
  
  
  Paso 2: Mirar el cluster recién creado
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ooel015gnezm45kl9mq.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%2F6ooel015gnezm45kl9mq.png" width="680" height="66"&gt;&lt;/a&gt;&lt;/p&gt;



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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faa2p3hzavm6kt0pws5kt.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%2Faa2p3hzavm6kt0pws5kt.png" width="537" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Auto Mode crea dos node pools automáticamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;system arranca con 1 nodo desde el inicio.&lt;/strong&gt; Ese nodo corre los componentes internos del propio cluster (CoreDNS y los add-ons gestionados). Por eso kubectl get nodes sí te muestra un nodo apenas creas el cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;general-purpose arranca con 0 nodos.&lt;/strong&gt; Este es el pool donde van a caer tus aplicaciones, y ahí es donde se ve la magia: se queda vacío hasta que despliegas algo que necesite cómputo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Así que el cambio de chip es este: ver el pool de tus cargas en cero no significa que algo se rompió. Significa que todavía no hay nada tuyo que correr, y AWS no está cobrándote nodos de más.&lt;/p&gt;



&lt;h3&gt;
  
  
  Paso 3: Desplegar una app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx-demo.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/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;Deployment&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;nginx-demo&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-demo&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;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-demo&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;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;nginx&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;nginx:latest&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;250m"&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;span class="nn"&gt;---&lt;/span&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;Service&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;nginx-demo&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-demo&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
    &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&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;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; nginx-demo.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5e4gplo1a74bpow4a37c.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%2F5e4gplo1a74bpow4a37c.png" width="380" height="70"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h3&gt;
  
  
  Paso 4: Ver Auto Mode en acción
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;En unos 30 segundos vas a ver aparecer un nodo nuevo en el pool general-purpose (ese que estaba en cero) sin que tú lo hayas pedido. AWS se dio cuenta de que tus pods necesitan cómputo y levantó una instancia EC2 solo.&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%2F3btj7alhozjck9eo5f26.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%2F3btj7alhozjck9eo5f26.png" alt=" " width="683" height="87"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h3&gt;
  
  
  Paso 5: Escalar y mirar qué pasa
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment nginx-demo &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Al escalar en numero de replicas de nuestra app, tener un solo nodo será insuficiente. Si la capacidad de ahora no alcanza, Auto Mode levanta más. No tocaste ningún autoscaler, no editaste ningún node group.&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%2Fbxtcup3zmi5nlz4o7z76.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%2Fbxtcup3zmi5nlz4o7z76.png" width="520" height="46"&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%2Fdvf0upi0vyoj3jyyx5k5.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%2Fdvf0upi0vyoj3jyyx5k5.png" width="683" height="116"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h3&gt;
  
  
  Paso 6: Destruir
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eksctl delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; demo-automode &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Es super importante que al finalizar nuestro laboratorio destruymos todos los recursos creado, para evitar costos innecesarios. Un cluster olvidado es una sorpresa fea en la factura.&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%2Fsvbm9w8xa0n2y83h59rl.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%2Fsvbm9w8xa0n2y83h59rl.png" alt=" " width="797" height="113"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  Clásico vs Auto Mode
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Qué&lt;/th&gt;
&lt;th&gt;EKS clásico&lt;/th&gt;
&lt;th&gt;EKS Auto Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gestión de nodos&lt;/td&gt;
&lt;td&gt;Manual / tu propio Karpenter&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add-ons (CoreDNS, CSI, etc.)&lt;/td&gt;
&lt;td&gt;Los instalas y versionas tú&lt;/td&gt;
&lt;td&gt;Gestionados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actualizar AMIs&lt;/td&gt;
&lt;td&gt;Lo gestionas tú&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoscaling&lt;/td&gt;
&lt;td&gt;Configuras CA o Karpenter&lt;/td&gt;
&lt;td&gt;Automático&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tu tiempo operando&lt;/td&gt;
&lt;td&gt;Mucho&lt;/td&gt;
&lt;td&gt;Poco&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personalización&lt;/td&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;Acotada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curva de arranque&lt;/td&gt;
&lt;td&gt;Empinada&lt;/td&gt;
&lt;td&gt;Suave&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Cuándo usarlo y cuándo no (Según mi opinión)
&lt;/h2&gt;

&lt;p&gt;Úsalo si:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arrancas un cluster nuevo y quieres velocidad sin construir toda una plataforma.&lt;/li&gt;
&lt;li&gt;Tu equipo es chico y no quieres tener a alguien dedicado a cuidar nodos.&lt;/li&gt;
&lt;li&gt;Tus workloads son bastante estándar: APIs, workers, web.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Piénsalo dos veces si:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependes de configuraciones muy específicas de nodo (kernels custom, DaemonSets exigentes, GPUs con drivers a medida).&lt;/li&gt;
&lt;li&gt;Ya tienes un Karpenter afinado que te ahorra dinero y no lo quieres soltar.&lt;/li&gt;
&lt;li&gt;Necesitas control total del ciclo de vida de la AMI por temas de compliance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auto Mode te cambia control por simplicidad. Ganas horas de operación, cedes algunos grados de personalización. Para la mayoría, ese cambio vale muchísimo la pena. Para casos muy puntuales, todavía no.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Y cuánto me va a costar?
&lt;/h2&gt;

&lt;p&gt;Auto Mode suma un pequeño cargo de gestión encima del costo del EC2 que corre por debajo. Pero no te quedes solo con "cuánto cuesta el add-on". La cuenta real es: cuánto cuesta el add-on menos las horas de ingeniería que dejas de gastar cuidando nodos, autoscalers y parches. Cuando sumas ese tiempo, el número se ve muy distinto.&lt;/p&gt;

&lt;h2&gt;
  
  
  En conclusión
&lt;/h2&gt;

&lt;p&gt;EKS Auto Mode no es magia. Es AWS haciendo el trabajo de plataforma que muchos veníamos haciendo a mano. Si gestionas clusters en producción, pruébalo con un workload que no sea crítico y mide cuánto tiempo dejas de invertir en cosas que no hacen mejor tu producto.&lt;/p&gt;

</description>
      <category>eksautomode</category>
      <category>aws</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS Has Two Iceberg REST Catalogs: What Each One Actually Serves</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 04 Sep 2026 18:29:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-has-two-iceberg-rest-catalogs-what-each-one-actually-serves-2bob</link>
      <guid>https://dev.to/aws-builders/aws-has-two-iceberg-rest-catalogs-what-each-one-actually-serves-2bob</guid>
      <description>&lt;p&gt;This article provides a step by step comparison of the two Apache Iceberg REST catalog implementations AWS ships — AWS Glue and Amazon S3 Tables — measuring what each one actually serves. A Python probe harness issues one identical request suite to both and stores the raw response as evidence.&lt;/p&gt;

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

&lt;p&gt;AWS ships two Iceberg REST catalogs. Both are managed, both are SigV4-signed, and both implement the same published specification. If you are choosing between them, or writing a client that has to work against either, the interesting question is where they diverge.&lt;/p&gt;

&lt;p&gt;They score identically and behave differently in thirteen places.&lt;/p&gt;

&lt;p&gt;All results below were measured on 2026-09-03.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the Iceberg REST Catalog?
&lt;/h2&gt;

&lt;p&gt;An Iceberg table is a directory of Parquet files plus a chain of JSON metadata files recording which files belong to the table right now. Something has to hold the pointer to the current metadata file, and make commits atomic by swapping it. That is the catalog.&lt;/p&gt;

&lt;p&gt;The REST catalog is one HTTP API for that job, so an engine needs one driver rather than one per catalog. The specification lives in the Iceberg repository as &lt;code&gt;open-api/rest-catalog-open-api.yaml&lt;/code&gt; and defines 35 operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/apache/iceberg/main/open-api/rest-catalog-open-api.yaml &lt;span class="nt"&gt;-o&lt;/span&gt; irc.yaml
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cE&lt;/span&gt; &lt;span class="s1"&gt;'^    (get|post|delete|head|put):'&lt;/span&gt; irc.yaml
&lt;span class="go"&gt;35
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This harness probes 25 of those 35, or 71%.&lt;/p&gt;

&lt;h2&gt;
  
  
  At This Point You Should Have
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account, and credentials with enough privilege to create a Glue database,
an S3 bucket and an S3 Tables table bucket&lt;/li&gt;
&lt;li&gt;Python 3.13 with &lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;pyiceberg&lt;/code&gt; and &lt;code&gt;botocore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;botocore&lt;/code&gt; available for SigV4 signing
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; Arn &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;span class="go"&gt;arn:aws:iam::AWS_ACCOUNT_ID:root
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="go"&gt;import sys, pyiceberg, requests, botocore
print('python    ', sys.version.split()[0])
print('pyiceberg ', pyiceberg.__version__)
print('requests  ', requests.__version__)
print('botocore  ', botocore.__version__)"
python     3.13.13
pyiceberg  0.12.0
requests   2.34.2
botocore   1.43.34
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything below was measured with account root, so no result here is a permissions artefact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Are the Two Endpoints?
&lt;/h2&gt;

&lt;p&gt;They are different services with different signing names.&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;AWS Glue&lt;/th&gt;
&lt;th&gt;Amazon S3 Tables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://glue.us-east-1.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://s3tables.us-east-1.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SigV4 signing name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;the account id&lt;/td&gt;
&lt;td&gt;the table bucket ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# GET /v1/config against Glue&lt;/span&gt;
&lt;span class="go"&gt;{"defaults":{"header.Content-Type":"application/x-amz-json-1.1","rest.sigv4-enabled":"true",
 "rest-table-scan-enabled":"true","prefix":"AWS_ACCOUNT_ID","rest.signing-region":"us-east-1",
 "rest.signing-name":"glue", ...}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Signing Is Where the First Hour Goes
&lt;/h2&gt;

&lt;p&gt;SigV4 signs the exact query string, so the URL you sign must be the URL you send. Building the URL and then letting an HTTP client re-encode the parameters separately produces a different canonical string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# signing one URL, sending another&lt;/span&gt;
&lt;span class="go"&gt;HTTP 403
{"message":"The request signature we calculated does not match the signature you
provided. Check your AWS Secret Access Key and signing method."}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build it once and send it whole. &lt;code&gt;urlencode&lt;/code&gt;'s default &lt;code&gt;quote_plus&lt;/code&gt; is also wrong here, because SigV4 wants &lt;code&gt;%20&lt;/code&gt; rather than &lt;code&gt;+&lt;/code&gt;. With both fixed, the same request reaches the service and returns a real answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# same probe, correct canonical query string&lt;/span&gt;
&lt;span class="go"&gt;HTTP 404
{"error":{"code":404,"message":"The specified bucket does not exist.","type":"no_such_bucket"}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 403 signature error and a 404 no-such-bucket look equally like failure in a log. Only one of them is about the catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing Up AWS Glue
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws s3api create-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$BUCKET&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws glue create-database &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    --database-input "{\"Name\":\"probe_ns\",\"LocationUri\":\"s3://$&lt;/span&gt;BUCKET/probe_ns/&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;aws glue get-databases --region us-east-1 --query 'DatabaseList[].Name' --output text
&lt;/span&gt;&lt;span class="go"&gt;probe_ns
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Glue rejects &lt;code&gt;createTable&lt;/code&gt; without an explicit table location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# createTable with no location field&lt;/span&gt;
&lt;span class="go"&gt;InvalidInputException: Location information cannot be null while creating an iceberg table
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every other catalog in this suite infers it from the warehouse. Glue does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing Up Amazon S3 Tables
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws s3tables create-table-bucket &lt;span class="nt"&gt;--name&lt;/span&gt; iceberg-probe &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --region us-east-1 --query arn --output text)
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ARN&lt;/span&gt;
&lt;span class="go"&gt;arn:aws:s3tables:us-east-1:AWS_ACCOUNT_ID:bucket/iceberg-probe
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws s3tables create-namespace &lt;span class="nt"&gt;--table-bucket-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --namespace probe_ns --region us-east-1
{"tableBucketARN": "arn:aws:s3tables:us-east-1:AWS_ACCOUNT_ID:bucket/iceberg-probe",
 "namespace": ["probe_ns"]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two constraints surface only when you hit them. Namespace names reject uppercase, which a timestamped scratch namespace will contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# createNamespace named irc_probe_20260903T1626&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400
The specified namespace name isn't valid. Specify a different namespace name, and
then try again.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;createTable&lt;/code&gt; requires &lt;code&gt;stage-create&lt;/code&gt; in the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# createTable without it&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400  stage-create is a required field and cannot be null
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeding a table also failed against the managed bucket with &lt;code&gt;pyiceberg&lt;/code&gt;'s default writer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# appending with the default PyArrow FileIO&lt;/span&gt;
&lt;span class="go"&gt;AWS Error [code 134] during CreateMultipartUpload operation: The authorization
mechanism you have provided is not supported. Please use Signature Version 4.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching to &lt;code&gt;pyiceberg.io.fsspec.FsspecFileIO&lt;/code&gt; worked. Note the bucket name in that error is not the one you created — S3 Tables stores data in a managed bucket of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prefixes Do Not Look Alike
&lt;/h2&gt;

&lt;p&gt;Every client reads the routing prefix from &lt;code&gt;/v1/config&lt;/code&gt; and puts it in every later URL. The two services return very different shapes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Catalog&lt;/th&gt;
&lt;th&gt;Prefix as returned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;catalogs/AWS_ACCOUNT_ID&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Tables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn%3Aaws%3As3tables%3Aus-east-1%3A...%3Abucket%2Ficeberg-probe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Glue returns two path segments. S3 Tables returns a percent-encoded ARN. A client that assumes one segment, or that re-encodes what it was handed, produces URLs neither service routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scores Are Identical
&lt;/h2&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;🥈 AWS Glue&lt;/th&gt;
&lt;th&gt;🥈 Amazon S3 Tables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read probes served&lt;/td&gt;
&lt;td&gt;9/15&lt;/td&gt;
&lt;td&gt;9/15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write probes served&lt;/td&gt;
&lt;td&gt;10/17&lt;/td&gt;
&lt;td&gt;10/17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not tested&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;loadTable&lt;/code&gt; fields present&lt;/td&gt;
&lt;td&gt;26/30&lt;/td&gt;
&lt;td&gt;27/30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints declared in &lt;code&gt;/v1/config&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read and write surfaces are scored separately rather than summed, and probes whose prerequisite failed are excluded rather than counted as failures.&lt;/p&gt;

&lt;p&gt;That table is the least interesting thing in this article. The two implementations arrive at the same totals by different routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neither One Tells You What It Supports
&lt;/h2&gt;

&lt;p&gt;The specification lets a server advertise its own surface:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;endpoints&lt;/strong&gt;: A list of endpoints that the server supports.&lt;/p&gt;

&lt;p&gt;— &lt;code&gt;rest-catalog-open-api.yaml&lt;/code&gt;, &lt;code&gt;CatalogConfig&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five of the seven catalogs in the wider comparison publish that array. Neither AWS catalog does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="go"&gt;import json
for c in ('aws-glue','aws-s3tables'):
    d = json.load(open('evidence/%s.json' % c))
    print('%-14s declares %s endpoints' % (c, len(d.get('declared_endpoints') or []) or 'no'))
"
aws-glue       declares no endpoints
aws-s3tables   declares no endpoints
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field is optional, so this is not a specification violation. It does mean capability discovery is unavailable on both, and a client has no way to learn what either serves short of probing it — which is what this harness does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirteen Probes Behave Differently
&lt;/h2&gt;

&lt;p&gt;Of 33 probes, 20 return the same verdict on both and 13 do not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Probe&lt;/th&gt;
&lt;th&gt;AWS Glue&lt;/th&gt;
&lt;th&gt;Amazon S3 Tables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_views&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_view&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;load_view&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replace_view&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rename_view&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drop_view&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rename_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 406 not supported&lt;/td&gt;
&lt;td&gt;✅ 204&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update_namespace_props&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ 200&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;plan_table_scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⚠️ 200 with exception&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;report_metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⚠️ 200 with exception&lt;/td&gt;
&lt;td&gt;❌ 400 not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commit_transaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⚠️ 200 with exception&lt;/td&gt;
&lt;td&gt;❌ 404 unknown operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drop_table_purge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 400 purge forbidden&lt;/td&gt;
&lt;td&gt;✅ 204&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drop_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ 204&lt;/td&gt;
&lt;td&gt;❌ 400 purge required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither implements views, but they refuse differently, and one of them renames tables while the other does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glue Names the Operation It Is Refusing
&lt;/h2&gt;

&lt;p&gt;Glue's refusals are specific, and they name the endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# POST .../views against Glue&lt;/span&gt;
&lt;span class="go"&gt;HTTP 406  CreateView endpoint is not supported for Glue Catalog.
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# POST .../tables/rename against Glue&lt;/span&gt;
&lt;span class="go"&gt;HTTP 406  RenameTable endpoint is not supported for Glue Catalog.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 Tables returns a bare XML document with no operation name and no JSON error body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# POST .../views against S3 Tables&lt;/span&gt;
&lt;span class="gp"&gt;HTTP 404  &amp;lt;UnknownOperationException/&amp;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 mean the same thing to a user and not to a program. Glue's &lt;code&gt;406&lt;/code&gt; with a named operation is machine-readable enough to log usefully; the bare exception is not.&lt;/p&gt;

&lt;p&gt;The exception to S3 Tables' silence is &lt;code&gt;report_metrics&lt;/code&gt;, which is the one place it says what it means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# POST .../tables/{table}/metrics against S3 Tables&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400  ReportMetrics is currently not supported.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Glue Answers Three Unrouted Operations With HTTP 200
&lt;/h2&gt;

&lt;p&gt;This is the finding a client author should care about most.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# POST .../tables/{table}/plan against Glue&lt;/span&gt;
&lt;span class="go"&gt;HTTP 200
&lt;/span&gt;&lt;span class="gp"&gt;{"Output": {"__type": "com.amazon.coral.service#&lt;/span&gt;UnknownOperationException&lt;span class="s2"&gt;"}, "&lt;/span&gt;Version&lt;span class="s2"&gt;": "&lt;/span&gt;1.0&lt;span class="s2"&gt;"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same body comes back from &lt;code&gt;report_metrics&lt;/code&gt; and &lt;code&gt;commit_transaction&lt;/code&gt;. The mechanism is the AWS protocol layer answering an operation its front door does not route, rather than Glue returning a broken success for an implemented endpoint.&lt;/p&gt;

&lt;p&gt;The consequence does not depend on the mechanism. Code that branches on the status code sees three endpoints that work, and only code that parses the body finds out otherwise. The harness gives these their own verdict rather than scoring them &lt;code&gt;OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Glue's &lt;code&gt;/v1/config&lt;/code&gt; also advertises &lt;code&gt;rest-table-scan-enabled: true&lt;/code&gt;, and scan planning is one of the three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Drop Requirements Are Opposite
&lt;/h2&gt;

&lt;p&gt;Glue refuses a purge drop. S3 Tables refuses a plain one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# DELETE .../tables/{table}?purgeRequested=true against Glue&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400  PurgeRequested cannot be true for Glue iceberg tables.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# DELETE .../tables/{table} against S3 Tables&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400  DropTable operation failed. S3 Tables only supports dropping tables with
purge enabled.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two products have different storage models, so this is defensible rather than a defect — Glue points at a bucket you own, and S3 Tables owns the storage it drops. It is still the sharpest example of why "AWS supports the Iceberg REST catalog" is not a sentence a client can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neither Supports Multi-Level Namespaces
&lt;/h2&gt;

&lt;p&gt;Both reject the &lt;code&gt;parent&lt;/code&gt; query parameter on &lt;code&gt;listNamespaces&lt;/code&gt;, and both say so plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# GET .../namespaces?parent=probe_ns against Glue&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400  Glue dataCatalog does not support multipart namespace.
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# the same against S3 Tables&lt;/span&gt;
&lt;span class="go"&gt;HTTP 400  Multipart namespaces are not supported.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where They Agree
&lt;/h2&gt;

&lt;p&gt;Twenty of the 33 probes return the same verdict, and the agreement is the core of the specification. Both serve config, namespace listing and loading, table listing and loading, &lt;code&gt;loadTable&lt;/code&gt; with full snapshot history, and both accept all five &lt;code&gt;updateTable&lt;/code&gt; actions probed — &lt;code&gt;set-properties&lt;/code&gt;, &lt;code&gt;remove-properties&lt;/code&gt;, &lt;code&gt;add-schema&lt;/code&gt;, &lt;code&gt;set-current-schema&lt;/code&gt; and &lt;code&gt;upgrade-format-version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both also return nearly identical &lt;code&gt;loadTable&lt;/code&gt; documents: 26 of 30 checked specification field paths on Glue, 27 on S3 Tables. The single extra on S3 Tables is a &lt;code&gt;config&lt;/code&gt; block. Whatever separates these two, it is not the fidelity of the metadata they return.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The goal of this article was to measure what AWS's two Iceberg REST catalog implementations actually serve, rather than what "supports the REST catalog" implies. The key to the solution was issuing one identical request suite to both and storing the raw response for every probe. The comparison results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identical totals, thirteen behavioural differences.&lt;/strong&gt; Both serve 9 of 15 read
probes and 10 of 17 write probes, and 20 of 33 probes agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither publishes an &lt;code&gt;endpoints&lt;/code&gt; declaration&lt;/strong&gt;, so capability discovery is
unavailable on both and a client must probe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glue answers three unrouted operations with HTTP 200&lt;/strong&gt; carrying an
&lt;code&gt;UnknownOperationException&lt;/code&gt;, one of which is the scan planning its own config
advertises as enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither implements views&lt;/strong&gt;, and they refuse differently — Glue with a &lt;code&gt;406&lt;/code&gt;
naming the operation, S3 Tables with a bare &lt;code&gt;&amp;lt;UnknownOperationException/&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The drop requirements are opposite.&lt;/strong&gt; Glue forbids &lt;code&gt;purgeRequested=true&lt;/code&gt;, S3
Tables requires it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rename_table&lt;/code&gt; works on S3 Tables and not on Glue.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: both catalogs probed once in us-east-1 on 2026-09-03 with account root, against one table shape seeded through pyiceberg 0.12.0, covering 25 of the specification's 35 operations, 5 of its 25 update actions and 1 of its 8 table requirements; neither service exposes a version, so no result here can be tied to a release; and the field tier records that a value is present, never that it is correct.&lt;/p&gt;

&lt;p&gt;The strategy for comparing two managed catalogs against one specification was validated with an incremental step by step approach.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iceberg</category>
      <category>lakehouse</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Build a Custom AWS Cognito Login Page with Passkeys</title>
      <dc:creator>Rishi</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:30:45 +0000</pubDate>
      <link>https://dev.to/aws-builders/build-a-custom-aws-cognito-login-page-with-passkeys-3a3g</link>
      <guid>https://dev.to/aws-builders/build-a-custom-aws-cognito-login-page-with-passkeys-3a3g</guid>
      <description>&lt;p&gt;Passkeys are replacing passwords. Cognito’s Hosted UI supports them out of the box. But Cognito also supports building a custom login page. I spent a weekend figuring out how to bring that same passkey experience to a custom login page.&lt;/p&gt;

&lt;p&gt;In this blog post, we will understand how passkeys work and how to create a custom passkey-supported login page using React and AWS Cognito. Along the way, we will also explore how things work under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Passkeys?
&lt;/h2&gt;

&lt;p&gt;Passwords are both difficult to remember and easy to lose. Passkeys bring a passwordless authentication paradigm based on public-key cryptography.&lt;/p&gt;

&lt;p&gt;You can think of a website’s login screen as a lock. Passkeys allow your device (or pass manager) to become the key for that lock.&lt;br&gt;
But… but… but…&lt;br&gt;
The interesting part is that the device can prove it has the key without ever handing the key over.&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%2F6784mrehs7pp7j0nk4ha.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%2F6784mrehs7pp7j0nk4ha.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During registration, a public-private key pair is created by your device (or passkey manager). The private key is kept protected on your device, and the public key is passed to the website.&lt;/p&gt;

&lt;p&gt;At the time of login, the website sends a fresh challenge to your device. After you verify yourself, your device uses the private key to &lt;strong&gt;sign the challenge&lt;/strong&gt;. The website then uses the public key to &lt;strong&gt;verify the signature&lt;/strong&gt;. If the signature is valid, you’re logged in.&lt;br&gt;
ie. Challenge → Sign with private key → Signature → Verify with public key → ✅&lt;/p&gt;

&lt;p&gt;When we use Cognito's Hosted UI, much of this flow is handled for us. But when we build our own login page, we need to connect our UI to the browser's passkey APIs and Cognito ourselves. And that's what we are going to do in the next step!&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up Amazon Cognito for Passkeys
&lt;/h2&gt;

&lt;p&gt;First of all, let us create a Cognito user pool with a public client. Then make the necessary changes to enable passkeys. If you are stuck at any step, please refer to the video for more details.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/95yH6xQk2G4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Step1: Go to client settings and make sure Choice-based sign-in: ALLOW_USER_AUTH is enabled. It makes sure entity logging in can select the sign-in method.&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%2F953rw14ej8yw5dhrr8vg.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%2F953rw14ej8yw5dhrr8vg.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step2: Go to User pool &amp;gt;&amp;gt; Authentication &amp;gt;&amp;gt;Authentication methods &amp;gt;&amp;gt; Passkey and modify the relying party ID. This should be the same as the domain of the login page. For me, it is “localhost” but if you are using a domain name, it will be like “example.com”.&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%2F0c8tv4qqt68iq557cv1y.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%2F0c8tv4qqt68iq557cv1y.png" alt=" " width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step3: Navigate to User pool &amp;gt;&amp;gt; Authentication &amp;gt;&amp;gt; Sign-in &amp;gt;&amp;gt; Options for choice-based sign-in. Then enable passkeys as an available sign-in choice.&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%2Flfdd6miuzb86opmofnhv.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%2Flfdd6miuzb86opmofnhv.png" alt=" " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, make sure MFA is either optional or disabled. And you are good to move on to the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Custom Login Page
&lt;/h2&gt;

&lt;p&gt;Frontend code is available here: &lt;a href="https://github.com/TrickSumo/cognito-custom-login-passkey-support" rel="noopener noreferrer"&gt;https://github.com/TrickSumo/cognito-custom-login-passkey-support&lt;/a&gt;&lt;br&gt;
Make sure to create a .env file with the details of the user pool created in the last step. Then install dependencies and run the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend uses @aws-sdk/client-cognito-identity-provider for Cognito API calls and @simplewebauthn/browser for handling the WebAuthn browser API calls for passkey registration and authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering a Passkey
&lt;/h2&gt;

&lt;p&gt;Create a Cognito user with the help of the signup form. After successful login, register a passkey using the “Add a passkey” button. The browser will prompt you to save the passkey on the device.&lt;/p&gt;

&lt;p&gt;This registration process involves two API calls (you can inspect them in the network tab).&lt;/p&gt;

&lt;p&gt;The first call is made to Cognito with the header &lt;strong&gt;“x-amz-target: AWSCognitoIdentityProviderService.StartWebAuthnRegistration”&lt;/strong&gt; and it initiates registration of a passkey. If you notice payload, it contains the access token that you received after successful login.&lt;/p&gt;

&lt;p&gt;The response of the first call contains the random challenge and other details:&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%2F3m90aspszc377fm860h1.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%2F3m90aspszc377fm860h1.png" alt=" " width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the browser’s &lt;code&gt;navigator.credentials.create()&lt;/code&gt; API is called to generate a new key pair on the device. The private key is stored securely on the device and never leaves it. The device then uses that private key to sign the challenge received from the first Cognito API call, proving a real authenticator generated this response. The new public key and the signed proof are then packaged together for the next step.&lt;/p&gt;

&lt;p&gt;Then the second API call goes to Cognito with the header &lt;strong&gt;“x-amz-target:AWSCognitoIdentityProviderService.CompleteWebAuthnRegistration”&lt;/strong&gt; and attestationObject/publicKey as the payload. This attestationObject is an encoded value that contains the public key, metadata about the authenticator, and the signature (computed over the clientDataJSON, which itself repeats the original challenge along with the page’s origin).&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%2Fvzu5jkveqa546tpkiq6j.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%2Fvzu5jkveqa546tpkiq6j.png" alt=" " width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;clientDataJSON after decoding from base64:&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%2Fg1eu4k5ccagptukdlec8.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%2Fg1eu4k5ccagptukdlec8.png" alt=" " width="554" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If all is good, the response from the second Cognito API is… Guess what? Empty JSON object {}.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing in with a Passkey
&lt;/h2&gt;

&lt;p&gt;Logging in with a passkey also involves two API calls to Cognito.&lt;/p&gt;

&lt;p&gt;The first one is an initiate auth API call with “PREFERRED_CHALLENGE: WEB_AUTHN”. It returns the challenge:&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%2Fsqtl9g0ebdeklpjypavo.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%2Fsqtl9g0ebdeklpjypavo.png" alt=" " width="800" height="300"&gt;&lt;/a&gt;&lt;br&gt;
Then the browser &lt;code&gt;navigator.credentials.get()&lt;/code&gt; API is called to produce a signed assertion using the private key.&lt;/p&gt;

&lt;p&gt;After that, the second API call to Cognito happens with the RespondToAuthChallenge header and signature as payload. Cognito verifies the payload using the public key that we stored during passkey registration. After successful login, accessToken, idToken, and refreshToken are issued.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Passkeys
&lt;/h2&gt;

&lt;p&gt;The same user can have multiple passkeys, and the good news is that Cognito makes it easy to manage passkeys.&lt;/p&gt;

&lt;p&gt;You can call Cognito with the ListWebAuthnCredentials and DeleteWebAuthnCredential header/command to list and delete existing passkeys.&lt;/p&gt;

&lt;p&gt;These functionalities are already available in the demo code (using the Cognito SDK). Feel free to explore!&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas!
&lt;/h2&gt;

&lt;p&gt;Make sure passkeys are enabled in the Cognito user pool. Also, the Relying Party ID must match your app’s domain.&lt;/p&gt;

&lt;p&gt;If MFA is set to “Required” on the user pool, passkeys quietly stop showing up as a sign-in option even if everything else is configured correctly. Keep MFA optional or off if you want passkeys to work.&lt;/p&gt;

&lt;p&gt;A passkey can only be registered by a user who’s already signed in ie. signup can’t create one directly&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&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%2Fpwd9dx17s38h3bbfadqh.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%2Fpwd9dx17s38h3bbfadqh.png" alt=" " width="617" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our implementation, we used Cognito APIs together with the browser’s WebAuthn APIs to register and authenticate users with passkeys.&lt;/p&gt;

&lt;p&gt;The nice part is that we don’t have to implement the cryptography ourselves. The browser and passkey provider handle the sensitive operations, while Cognito handles authentication and token issuance.&lt;/p&gt;

&lt;p&gt;Hope you enjoyed the tutorial 😊&lt;/p&gt;

</description>
      <category>webauthn</category>
      <category>aws</category>
      <category>cognito</category>
      <category>security</category>
    </item>
    <item>
      <title>I Took the Wheel Off My RAG: Managed KB vs. Manual Chunking</title>
      <dc:creator>Gerardo Arroyo</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:39:17 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-took-the-wheel-off-my-rag-managed-kb-vs-manual-chunking-106m</link>
      <guid>https://dev.to/aws-builders/i-took-the-wheel-off-my-rag-managed-kb-vs-manual-chunking-106m</guid>
      <description>&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%2Fumw14862b8efn47e7izr.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%2Fumw14862b8efn47e7izr.jpg" alt="Cover" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In April I published a benchmark comparing 5 chunking strategies in Amazon Bedrock Knowledge Bases. The practical conclusion was simple: &lt;code&gt;FIXED_SIZE&lt;/code&gt; with S3 Vectors as the backend, and that's it, unless your data justifies something more complex.&lt;/p&gt;

&lt;p&gt;Three months later, AWS took the question away from the problem entirely. Amazon Bedrock Managed Knowledge Base doesn't ask you to pick a chunking strategy. Smart Parsing decides parsing by document type, AWS manages the vector store, and if your questions are complex, &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; plans a retrieval strategy instead of running a single similarity search.&lt;/p&gt;

&lt;p&gt;Sounds like April's discussion is obsolete. So I asked the question that felt honest to me: &lt;strong&gt;how much does it cost in quality to have the wheel taken away from you, and under what conditions do you get that loss back?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I didn't have a thesis before running this. I have one now, and it's not the one I expected.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;Spoiler&lt;/strong&gt;: Simple managed retrieval practically ties my manual April configuration. What didn't work is the agentic planner —the piece that justifies the "agentic" in the name— with the model AWS gives you by default: &lt;strong&gt;it didn't decompose a single query across 80 evaluated questions&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article covers the full methodology: five retrieval configurations over the same corpus, two question sets —the original 25 single-hop questions from April untouched, and 15 new multi-hop questions written against the actual document text—, three different planners, and the 6 infrastructure gotchas I had to resolve before I could measure anything. Because the planner finding has a twist: it depends entirely on which model does the planning, and I didn't know that until I stopped using the one that ships by default.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📌 TL;DR — Key data before you keep reading&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed KB with simple &lt;code&gt;Retrieve&lt;/code&gt;&lt;/strong&gt; ≈ my manual April configuration in Correctness (0.88 vs. 0.84, a difference within the expected range).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Parsing ingests without failing&lt;/strong&gt; the two PDFs that broke &lt;code&gt;SEMANTIC&lt;/code&gt; and &lt;code&gt;NONE&lt;/code&gt; in April — and does it faster than my manual pipeline (183s vs. 407s). It even ingests and correctly retrieves a PDF with no extractable text layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AgenticRetrieveStream&lt;/code&gt; with the default planner (&lt;code&gt;MANAGED&lt;/code&gt;) never generated a single sub-query&lt;/strong&gt; across 80/80 evaluated questions, including ones a simple retrieval demonstrably couldn't solve.&lt;/li&gt;
&lt;li&gt;On multi-hop questions, that made the "agentic" retriever land &lt;strong&gt;below&lt;/strong&gt; simple retrieval (0.50 vs. 0.567 Correctness).&lt;/li&gt;
&lt;li&gt;With a small &lt;code&gt;CUSTOM&lt;/code&gt; planner (Claude Haiku 4.5), real decomposition does show up — but on questions that no longer needed it, not on the 5 that did.&lt;/li&gt;
&lt;li&gt;With a &lt;strong&gt;large&lt;/strong&gt; &lt;code&gt;CUSTOM&lt;/code&gt; planner (Claude Sonnet 4.6, the same model as my generator), multi-hop Correctness jumped from 0.50 to &lt;strong&gt;1.00&lt;/strong&gt;, sustained across two independent runs — but the planner and generator share the same model in that test, and I'm stating that explicitly as a caveat, not as fine print.&lt;/li&gt;
&lt;li&gt;The corpus &lt;strong&gt;isn't byte-identical&lt;/strong&gt; to April's: my original bucket was destroyed and I had to re-download the documents. I'm saying that explicitly, not hiding it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Matters to Me
&lt;/h2&gt;

&lt;p&gt;This isn't academic curiosity. If you're evaluating whether to migrate a production RAG to Managed Knowledge Base, the real question isn't "is it better?" — it's "what do I lose by giving up control of chunking, and what do I gain if I also let the service decide when and how to search more than once?"&lt;/p&gt;

&lt;p&gt;That second part is what almost nobody measures. AWS presents &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; with numbers from an academic benchmark (MuSiQue) showing gains of up to +37 recall points on 4-hop questions. Those are real numbers, published on their &lt;a href="https://aws.amazon.com/blogs/machine-learning/agentic-retrieval-for-amazon-bedrock-managed-knowledge-base/" rel="noopener noreferrer"&gt;official agentic retrieval blog post&lt;/a&gt;, and I break them down further down. But an academic benchmark doesn't tell you what happens when the planner you get &lt;strong&gt;by default&lt;/strong&gt;, without touching a single parameter, meets your own corpus.&lt;/p&gt;

&lt;p&gt;That's what I ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design: Five Configurations, Two Question Sets
&lt;/h2&gt;

&lt;p&gt;I kept the generator (Claude Sonnet 4.6) and the judge (Nova Pro, cross-family) constant across four of the five configurations —the only exception is D, where the service itself generates— to isolate the retrieval layer as the sole variable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Retrieval&lt;/th&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3 Vectors + &lt;code&gt;FIXED_SIZE&lt;/code&gt; (April)&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed KB, &lt;code&gt;Retrieve&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Did I lose quality by handing over the wheel?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed KB, &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; (&lt;code&gt;generateResponse=False&lt;/code&gt;, planner &lt;code&gt;MANAGED&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Does the default planner work?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed KB, &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; (&lt;code&gt;generateResponse=True&lt;/code&gt;, planner &lt;code&gt;MANAGED&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The service itself&lt;/td&gt;
&lt;td&gt;What if I let AWS generate too?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed KB, &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; (&lt;code&gt;generateResponse=False&lt;/code&gt;, planner &lt;code&gt;CUSTOM&lt;/code&gt; = Sonnet 4.6)&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Does a &lt;em&gt;large&lt;/em&gt; planner help, not just a different one from the managed default?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first four ran together. I added E a day later, as a direct response to what I found with C — I cover it in detail further down, in its own section, so as not to mix an exploratory run with the original pre-registered design.&lt;/p&gt;

&lt;p&gt;And two question sets over the same corpus of 3 technical documents (the Well-Architected Framework, the AgentCore developer guide, and a RAG evaluation blog post):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1-hop set&lt;/strong&gt;: the 25 original single-hop questions from April. Untouched, for comparability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-hop set&lt;/strong&gt;: 15 new multi-hop and comparative questions, written against the actual document text, not against summaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔍 &lt;strong&gt;ProTip #1&lt;/strong&gt;: If you're going to compare an "agentic" retriever against a simple one, you need two question sets, not one. AWS itself reports that its gain on single-hop questions is under 5 recall points. Running only single-hop questions against a retriever that plans is measuring something you already know in advance won't show a difference — that's not a benchmark, it's a confirmation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A fairness control that cost me time but was non-negotiable: &lt;strong&gt;I re-ran configuration A in full&lt;/strong&gt;, I didn't reuse the scores published in April. Those came from the native &lt;code&gt;retrieveAndGenerate&lt;/code&gt; path; B, C, and D can only be evaluated through the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-evaluation-create-randg.html" rel="noopener noreferrer"&gt;&lt;em&gt;bring your own inference responses&lt;/em&gt;&lt;/a&gt; (BYOI) path. Comparing scores produced by two different evaluation mechanisms would have been exactly the kind of methodological trap I criticize other benchmarks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Gotcha, Before Writing a Single Line of Retrieval Code
&lt;/h2&gt;

&lt;p&gt;My original plan was to reuse April's Knowledge Base as-is. I couldn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;April's KB and its corpus bucket no longer existed.&lt;/strong&gt; That wasn't an oversight — I'd intentionally destroyed them after publishing the article, as I do with almost all my benchmark infrastructure. Verified against the real account before assuming anything: &lt;code&gt;list-knowledge-bases&lt;/code&gt; returned zero results across five regions, the S3 Vectors bucket had no buckets, and the corpus bucket returned &lt;code&gt;NoSuchBucket&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had to recreate just the &lt;code&gt;FIXED_SIZE&lt;/code&gt; module from April's repo (Titan v2, 1024 dimensions, 512-token chunks, 20% overlap) — without the other 4 chunking modules, which weren't needed for this benchmark.&lt;/p&gt;

&lt;p&gt;And here's the uncomfortable part I do need to say: &lt;strong&gt;the corpus couldn't be byte-identical to April's&lt;/strong&gt;. The data folder was in the &lt;code&gt;.gitignore&lt;/code&gt; of the original repo — I never versioned the actual content, so there was no hash or manifest to compare against. Verified by HTTP HEAD:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;April (per README)&lt;/th&gt;
&lt;th&gt;Today&lt;/th&gt;
&lt;th&gt;Conclusion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bedrock-agentcore-dg.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~17 MB&lt;/td&gt;
&lt;td&gt;30,420,374 bytes&lt;/td&gt;
&lt;td&gt;Nearly double. Confirmed NOT identical.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wellarchitected-framework.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~14 MB&lt;/td&gt;
&lt;td&gt;14,189,927 bytes&lt;/td&gt;
&lt;td&gt;Similar size, but without a hash I can't claim exact identity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blog-rag-evaluation.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Modified August 18&lt;/td&gt;
&lt;td&gt;Touched after April.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The AgentCore developer guide practically doubled in size over four months, which makes sense — it's the service that's evolved the most in that period. I'm reporting this as an explicit limitation of the re-run. I'm not hiding it, not minimizing it, and I also don't think it invalidates the comparison: the documents are still the same &lt;em&gt;class&lt;/em&gt; of content (dense AWS technical documentation), which is what April's benchmark needed to be representative.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;ProTip #2&lt;/strong&gt;: If you're going to publish a benchmark you plan to revisit months later, version the corpus with a public hash (even if you don't upload the full files). I didn't do that in April, and it cost me the ability to claim "byte-identical" with evidence, not just intent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Second Batch of Gotchas: Creating and Querying a Managed KB Isn't Like a Regular One
&lt;/h2&gt;

&lt;p&gt;Six real infrastructure problems, none of them documented together anywhere I could find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;Retrieve&lt;/code&gt; against a Managed KB rejects &lt;code&gt;vectorSearchConfiguration&lt;/code&gt;.&lt;/strong&gt; My April code used that parameter with no issue against the S3 Vectors KB. Against the Managed KB, the service responded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValidationException: Incompatible configuration: vectorSearchConfiguration
is not supported for managed knowledge bases. Use managedSearchConfiguration
instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same internal shape, different container key. Config A uses one, config B needs the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Creating an S3 data source with &lt;code&gt;type=S3&lt;/code&gt; fails on a Managed KB.&lt;/strong&gt; The error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValidationException: Unsupported data source type for MANAGED knowledge
base type.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct form —which I only found against a real AWS example, not against the shape's reference documentation— is &lt;code&gt;type=MANAGED_KNOWLEDGE_BASE_CONNECTOR&lt;/code&gt;, with the connector's configuration nested one level deeper than feels intuitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Creation is asynchronous in a way I didn't expect.&lt;/strong&gt; With Terraform, config A accepts &lt;code&gt;CreateKnowledgeBase&lt;/code&gt; and &lt;code&gt;CreateDataSource&lt;/code&gt; back-to-back with no waiting. Against a Managed KB, calling &lt;code&gt;CreateDataSource&lt;/code&gt; while the KB is still &lt;code&gt;CREATING&lt;/code&gt; fails with &lt;code&gt;ConflictException&lt;/code&gt;. You have to poll until &lt;code&gt;AVAILABLE&lt;/code&gt; before continuing — typically 2-5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;ragSourceIdentifier&lt;/code&gt; in the eval job isn't a free-form label.&lt;/strong&gt; I tried giving it a descriptive name (&lt;code&gt;"D-setA"&lt;/code&gt;) and the service rejected it: it has to match &lt;strong&gt;exactly&lt;/strong&gt; the &lt;code&gt;knowledgeBaseIdentifier&lt;/code&gt; carried by each line of the BYOI dataset, or it fails with &lt;code&gt;ValidationException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The eval job's output schema is different from the one documented for "automated MODEL evaluation".&lt;/strong&gt; That documented schema uses &lt;code&gt;automatedEvaluationResult.scores&lt;/code&gt; at the root. The real one, confirmed against raw output in S3, is &lt;code&gt;conversationTurns[].results[]&lt;/code&gt;, with a single JSONL per job at a path the service generates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. The judge sometimes can't extract a score.&lt;/strong&gt; A real example, verified against the raw JSONL: for the question "Summarize the design principles of the Operational Excellence pillar" (config A, 1-hop set), Nova Pro answered in natural language with a perfectly coherent explanation, but &lt;code&gt;Builtin.Faithfulness&lt;/code&gt; came back &lt;code&gt;result: null&lt;/code&gt; with the message &lt;code&gt;Unable to parse score from the LLM judge response&lt;/code&gt;. It's not that the judge failed to reason — the service simply couldn't parse a number out of that specific response. I'm reporting it and counting it in the final tally, not averaging it away by hiding 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%2F47ogm1dos4rykjv81eyd.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%2F47ogm1dos4rykjv81eyd.png" alt="Example conversation view in the Bedrock Evaluations console showing the judge's response and the Unable to parse score from the LLM judge response error" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: The judge's response is coherent and correctly lists the principles, but the service failed to extract a numeric score from that text. The error is documented right in the console.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With that resolved, I was able to confirm something that broke in my favor: &lt;strong&gt;Smart Parsing ingests without complaint&lt;/strong&gt; the two PDFs that broke &lt;code&gt;SEMANTIC&lt;/code&gt; (1 MB limit) and &lt;code&gt;NONE&lt;/code&gt; (50,000-character limit) in April. The job finished &lt;code&gt;COMPLETE&lt;/code&gt;, 3 of 3 documents indexed, 0 failed, in roughly 183 seconds — faster than my manual FIXED_SIZE + S3 Vectors pipeline over the same corpus (~407 seconds).&lt;/p&gt;

&lt;p&gt;I went a step further to really stress-test Smart Parsing: I generated a synthetic 2-page PDF &lt;strong&gt;with no extractable text layer at all&lt;/strong&gt; —confirmed with &lt;code&gt;pypdf&lt;/code&gt; that &lt;code&gt;extract_text()&lt;/code&gt; returns an empty string on both pages, only content rasterized to image— and added it to the corpus. The ingestion job finished &lt;code&gt;COMPLETE&lt;/code&gt; all the same, no failures, no skipped documents. And it didn't stop at "accepted it in name only": a direct &lt;code&gt;Retrieve&lt;/code&gt; with a query about that PDF's content returned the correct text as the top result, score 0.981, pointing exactly at the file. The &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html" rel="noopener noreferrer"&gt;Knowledge Bases documentation&lt;/a&gt; does list scanned documents among the types Smart Parsing automatically selects for, but it doesn't name the mechanism: that it runs OCR internally is something I'm inferring from the result, not from an explicit promise.&lt;/p&gt;

&lt;p&gt;That's already a data point with real weight: if your corpus has large or scanned files that break manual chunking, Managed KB solves a real problem for you without you having to think about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;Nova Pro judge and BYOI across all configurations, with the same scripts for A, B, C, and D. A single run per cell — not averaged over multiple executions, so take it as signal, not statistical certainty.&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%2Fmkeeajhxvk7wrve12ssz.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%2Fmkeeajhxvk7wrve12ssz.png" alt="List of 8 completed RAG evaluation jobs in the Amazon Bedrock console, one for each configuration/question-set combination" width="799" height="355"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: All 8 combinations of the original design (configurations A-D × 2 sets) finished &lt;code&gt;Completed&lt;/code&gt; in the RAG evaluations console. None got stuck halfway or failed silently.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;th&gt;Correctness&lt;/th&gt;
&lt;th&gt;Completeness&lt;/th&gt;
&lt;th&gt;Faithfulness&lt;/th&gt;
&lt;th&gt;Helpfulness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;single-hop (n=25)&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;0.875&lt;/td&gt;
&lt;td&gt;0.847&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;single-hop (n=25)&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.880&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;single-hop (n=25)&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;0.867&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;single-hop (n=25)&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.940&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;multi-hop (n=15)&lt;/td&gt;
&lt;td&gt;0.733&lt;/td&gt;
&lt;td&gt;0.783&lt;/td&gt;
&lt;td&gt;0.783&lt;/td&gt;
&lt;td&gt;0.822&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;multi-hop (n=15)&lt;/td&gt;
&lt;td&gt;0.567&lt;/td&gt;
&lt;td&gt;0.517&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.778&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;multi-hop (n=15)&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;0.778&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;multi-hop (n=15)&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;0.878&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;single-hop (n=25)&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;td&gt;0.873&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;multi-hop (n=15)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.883&lt;/td&gt;
&lt;td&gt;0.917&lt;/td&gt;
&lt;td&gt;0.911&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row E ran a day after A-D, as a direct follow-up to the planner finding — the full detail, including the caveat that the planner and generator share the same model there, is in its own section further down.&lt;/p&gt;

&lt;p&gt;And the verdict on the five hypotheses I pre-registered before running anything:&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;Hypothesis&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H1&lt;/td&gt;
&lt;td&gt;B ≈ A in Correctness (single-hop), ±0.05&lt;/td&gt;
&lt;td&gt;0.88 − 0.84 = 0.04&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consistent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2&lt;/td&gt;
&lt;td&gt;C ≈ B on single-hop (the planner shouldn't add anything)&lt;/td&gt;
&lt;td&gt;Identical Correctness: 0.88 = 0.88&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consistent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H3&lt;/td&gt;
&lt;td&gt;C &amp;gt; B by a wide margin on multi-hop&lt;/td&gt;
&lt;td&gt;0.50 &amp;lt; 0.567 — C lands below B&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Refuted&lt;/strong&gt; (with the &lt;code&gt;MANAGED&lt;/code&gt; planner)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H4&lt;/td&gt;
&lt;td&gt;C's latency ≥ 3× B's&lt;/td&gt;
&lt;td&gt;~2.7s vs. ~0.6-0.9s on single-hop; 3.07s vs. 0.67s on multi-hop (4.6×)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consistent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H5&lt;/td&gt;
&lt;td&gt;D ≤ C in Faithfulness&lt;/td&gt;
&lt;td&gt;0.86 ≤ 0.94 on single-hop; tied at 0.55 on multi-hop&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consistent&lt;/strong&gt;&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%2F7qqyx7so8853a1ilndo9.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%2F7qqyx7so8853a1ilndo9.png" alt="Amazon Bedrock console comparison between config B and config C on the multi-hop set: Correctness 0.57 vs 0.50, Completeness 0.52 vs 0.50, Faithfulness 0.60 vs 0.55, Helpfulness 0.78 vs 0.78" width="800" height="659"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: RAG evaluations' native comparison view, config B (simple &lt;code&gt;Retrieve&lt;/code&gt;) against config C (&lt;code&gt;AgenticRetrieveStream&lt;/code&gt;) on the multi-hop set. The "agentic" retriever loses on 3 of 4 metrics and ties on the fourth.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Four of five hypotheses held. The one that fell is the interesting one, and it didn't fall by a little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a scope caveat on H3.&lt;/strong&gt; What that hypothesis evaluates is the &lt;code&gt;MANAGED&lt;/code&gt; planner, the default. Further down you'll see that with a large &lt;code&gt;CUSTOM&lt;/code&gt; planner, the pattern H3 predicted does show up — and that doesn't rescue it, because it was refuted exactly as I wrote it, for the planner it measured. What it does leave is the question I chase through the rest of the article: is what matters the planner's size, or the fact that a large planner ends up sharing a model with the generator?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Main Finding: The Planner Never Planned
&lt;/h2&gt;

&lt;p&gt;H3 isn't a "didn't help much." It's a measured regression: on the question set where the agentic retriever should shine, it ended up &lt;strong&gt;worse&lt;/strong&gt; than single-step simple retrieval.&lt;/p&gt;

&lt;p&gt;And it has an identified mechanical cause, not a statistical mystery. I reviewed the full trace of all 80 agentic questions run (configurations C and D, both full sets). In &lt;strong&gt;100% of cases&lt;/strong&gt;, the structure was identical: a single &lt;code&gt;Retrieval&lt;/code&gt; step —the speculative pass with the raw query— followed by a &lt;code&gt;Planning&lt;/code&gt; step that returned &lt;code&gt;"actions": []&lt;/code&gt;. A second &lt;code&gt;Retrieval&lt;/code&gt; step never appeared. &lt;code&gt;FullDocumentExpansion&lt;/code&gt; never appeared. The planner &lt;strong&gt;never generated a single sub-query&lt;/strong&gt;, not even on the multi-hop set questions where I already knew —from a separate check I did before running the benchmark— that single-step retrieval didn't reach both relevant documents.&lt;/p&gt;

&lt;p&gt;A concrete example. For the question &lt;em&gt;"which AgentCore component applies logic similar to the Security pillar's 'Keep people away from data' for code execution?"&lt;/em&gt;, the initial speculative &lt;code&gt;Retrieval&lt;/code&gt; brought back only content from the AgentCore developer guide —Code Interpreter, session isolation, security controls— and nothing from the Well-Architected Framework. The subsequent &lt;code&gt;Planning&lt;/code&gt; step decided that was already enough.&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%2Ftvtbxr6l3rc45ykbl2ps.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%2Ftvtbxr6l3rc45ykbl2ps.png" alt="Raw AgenticRetrieveStream trace showing a Retrieval step with status SUCCEEDED, followed by two Planning events where the second returns actions: [] with status SUCCEEDED" width="799" height="543"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4: Raw trace for this same question. After the single &lt;code&gt;Retrieval&lt;/code&gt; step, the &lt;code&gt;Planning&lt;/code&gt; event with &lt;code&gt;"message": "Agent planning completed"&lt;/code&gt; returns &lt;code&gt;"actions": []&lt;/code&gt; — the planner considered the work done without generating a single sub-query.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 &lt;strong&gt;ProTip #3&lt;/strong&gt;: If you're going to measure "iterations" of an agentic retriever, don't count trace steps — count real sub-queries. A &lt;code&gt;Planning&lt;/code&gt; step that returns &lt;code&gt;actions: []&lt;/code&gt; still counts as an iteration in the &lt;code&gt;iterations&lt;/code&gt; field the SDK exposes, but it doesn't represent any decomposition work. It's the difference between "the planner ran" and "the planner planned".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Is This a Bug? I Went to Verify It Against the Source
&lt;/h3&gt;

&lt;p&gt;Before publishing this as a finding, I checked it against the official AWS post dedicated entirely to this capability, &lt;a href="https://aws.amazon.com/blogs/machine-learning/agentic-retrieval-for-amazon-bedrock-managed-knowledge-base/" rel="noopener noreferrer"&gt;Agentic retrieval for Amazon Bedrock Managed Knowledge Base&lt;/a&gt; — the same one that's the source of the MuSiQue numbers I cite above. Three things get confirmed, one gets nuanced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It confirms this isn't a problem with my data capture.&lt;/strong&gt; The documentation describes the &lt;code&gt;Retrieval&lt;/code&gt;/&lt;code&gt;FullDocumentExpansion&lt;/code&gt; step as "one event per executed sub-query." My code logs any step that arrives in the stream, with no name filtering. If the planner had decomposed even once across 80 questions, it would have shown up in the trace. It never did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It confirms that exiting early is documented behavior, not a bug.&lt;/strong&gt; The docs explicitly say the planner "may exit early after its evaluation step determines the evidence is sufficient." 100% of my cases are exactly that — a normal service state, not a failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It confirms the MuSiQue numbers I already cited&lt;/strong&gt;, with their exact values: +22.8, +31.9, and +37.3 recall points on 2-, 3-, and 4-hop questions, with gains under 5 points on single-hop questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it nuances something important&lt;/strong&gt;: the post's two official code examples —single-KB and multi-KB— use &lt;code&gt;foundationModelType: "CUSTOM"&lt;/code&gt; with an explicit model. &lt;strong&gt;Neither uses the default value (&lt;code&gt;MANAGED&lt;/code&gt;)&lt;/strong&gt;. The same post recommends as best practice "starting with a small, fast planner," which implies deliberately choosing a model, not trusting the default.&lt;/p&gt;

&lt;p&gt;That leads me to the most honest conclusion I can give: I can't claim "&lt;code&gt;AgenticRetrieveStream&lt;/code&gt; doesn't decompose" in general. I can only claim, with evidence from 80 of 80 questions, that &lt;strong&gt;the default managed planner —the one anyone who hasn't read this AWS blog post gets— never exercised its decomposition capability on my corpus&lt;/strong&gt;, not even when the retrieved evidence was insufficient. That all the official reference documentation uses &lt;code&gt;CUSTOM&lt;/code&gt; is, in itself, a data point: it suggests AWS doesn't expect the managed model to reliably decompose either, without the user choosing their own planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Tried the Obvious Fix: My Own Planner
&lt;/h2&gt;

&lt;p&gt;I configured &lt;code&gt;foundationModelType=CUSTOM&lt;/code&gt; with Claude Haiku 4.5 as the planner —a "small, fast" model, exactly what AWS recommends, and different from both the generator (Sonnet 4.6) and the judge (Nova Pro), so as not to contaminate either role.&lt;/p&gt;

&lt;p&gt;I kept this run separate; it doesn't replace configuration C's official results, which remain with the managed planner, because that's what the benchmark's design deliberately measures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something did change.&lt;/strong&gt; With Haiku 4.5 as the planner, 3 of the 15 multi-hop set questions showed &lt;code&gt;planning_actions &amp;gt; 0&lt;/code&gt; — the planner generated real sub-queries, something that never happened even once across the 80 questions with the managed planner. On the single-hop set, 0 of 25 decomposed, which is consistent: it isn't needed there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But it didn't change what actually mattered.&lt;/strong&gt; The 5 cross-document questions where simple &lt;code&gt;Retrieve&lt;/code&gt; demonstrably failed to bring back evidence from both documents —I separately confirmed the missing content was properly indexed, this wasn't an ingestion problem— &lt;strong&gt;still came back with a single document, exactly like with the managed planner&lt;/strong&gt;. The CUSTOM planner decomposed questions that already had sufficient evidence in a single pass, and didn't touch the ones that genuinely needed both sources.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;ProTip #4&lt;/strong&gt;: Switching from the managed planner to your own isn't a magic button that fixes cross-document multi-hop, at least not with a "small, fast" model like AWS recommends. The publishable result, with what I have so far, is: &lt;strong&gt;changing the planner helps the planner work, it doesn't guarantee it works on the right questions.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One obvious question remained unanswered: what if the planner isn't just different, but &lt;em&gt;large&lt;/em&gt;? I tested it, and the result made me rewrite this section twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Went Up a Weight Class: A Large Planner (And a Role Collision I Need to Name)
&lt;/h2&gt;

&lt;p&gt;AWS's official blog mentions two candidates as a "large" planner: Claude Sonnet 4.6 and Amazon Nova Premier. Before running anything, I checked which one was still viable against the real account, not against the docs — and found a problem.&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%2Fecdakrc0we9gql5zf2d2.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%2Fecdakrc0we9gql5zf2d2.png" alt="Nova Premier card in the Amazon Bedrock model catalog showing the Legacy label" width="591" height="358"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 5: Nova Premier shows up marked &lt;code&gt;Legacy&lt;/code&gt; in the model catalog. Verified in more detail: &lt;code&gt;startOfLifeTime=2025-04-30&lt;/code&gt;, &lt;code&gt;legacyTime=2026-03-13&lt;/code&gt;, &lt;code&gt;endOfLifeTime=2026-09-14&lt;/code&gt; — twelve days after this session.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nova Premier gets ruled out not by design preference, but because anyone trying to reproduce this benchmark after September 14 couldn't use it even if they wanted to. That left me with just one "large" alternative cited by AWS: &lt;strong&gt;Sonnet 4.6, the same model I already use as the constant generator in A, B, C, and D&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I need to be explicit here about what this breaks: my own design rule for the Haiku run was to keep the planner isolated from the generator and judge, precisely so as not to contaminate any role. Config E breaks that rule on purpose, documented as a deviation, not an oversight. The result that follows can't be read as "pure large planner" — it has to be read as "large planner that's also the same model that generates the response afterward".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With that caveat already on the table, the numbers:&lt;/strong&gt;&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;
&lt;code&gt;MANAGED&lt;/code&gt; (C)&lt;/th&gt;
&lt;th&gt;Haiku 4.5 (CUSTOM, exploratory)&lt;/th&gt;
&lt;th&gt;Sonnet 4.6 (E)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-hop set, questions with real sub-queries&lt;/td&gt;
&lt;td&gt;0/25&lt;/td&gt;
&lt;td&gt;0/25&lt;/td&gt;
&lt;td&gt;4/25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop set, questions with real sub-queries&lt;/td&gt;
&lt;td&gt;0/15&lt;/td&gt;
&lt;td&gt;3/15&lt;/td&gt;
&lt;td&gt;8/15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sonnet 4.6 decomposed almost three times as often as Haiku. Of the 5 cross-document questions that neither &lt;code&gt;MANAGED&lt;/code&gt; nor Haiku solved (b04, b08, b09, b11, b14), verified by searching for Well-Architected Framework-specific vocabulary in the retrieved chunks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;b04 and b14 get fixed&lt;/strong&gt; — they now do bring back WAF content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;b08 partially improves&lt;/strong&gt; — some WAF, not as much as the previous two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;b09 and b11 still miss the side that was missing&lt;/strong&gt; — b09 even generated a sub-query, but not toward the right document; b11 didn't even decompose.&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%2Fz45bx0v6l2clxgqb67ue.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%2Fz45bx0v6l2clxgqb67ue.png" alt="View of conversation 14 from config E's eval job in the Bedrock console: the question crosses the Well-Architected Framework's Maximize utilization principle with AgentCore Runtime's Instances mode, the answer cites passages 11 and 13, and the Correctness column shows 1" width="800" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 6: b14 solved with the Sonnet 4.6 planner. The question crosses the Well-Architected Framework's sustainability principle &lt;code&gt;Maximize utilization&lt;/code&gt; with AgentCore Runtime's &lt;code&gt;Instances&lt;/code&gt; mode — two different documents. The answer cites retrieved passages 11 and 13, matches the ground truth, and the judge gives it Correctness 1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In other words: a bigger planner decomposes more, but doesn't guarantee it decomposes &lt;em&gt;well&lt;/em&gt;. Two of five problem questions are still broken even with the most capable model I could test.&lt;/p&gt;

&lt;p&gt;And the eval job with Nova Pro, same 4 metrics:&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%2Fk5dwf7q9yqlyhkx08no0.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%2Fk5dwf7q9yqlyhkx08no0.png" alt="Bedrock console radar chart comparing config C's eval job against config E's on the multi-hop set, with E's polygon extending beyond C's on Correctness, Completeness, Helpfulness, and Faithfulness" width="800" height="751"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 7: Native comparison of the two eval jobs on the multi-hop set. E's area dominates C's on all four measured metrics, with the widest gap in Correctness. The other radar axes sit at 0 because those evaluators weren't part of the job.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the single-hop set the difference is small (0.90 vs. 0.88 Correctness), consistent with H2: a planner, whatever its size, shouldn't add much when a single step is already enough. On the multi-hop set the jump is large: Correctness goes from 0.50 (C) to &lt;strong&gt;1.00&lt;/strong&gt; (E) — above even B (simple &lt;code&gt;Retrieve&lt;/code&gt;, 0.567) and D (&lt;code&gt;MANAGED&lt;/code&gt; planner + service generation, 0.70).&lt;/p&gt;

&lt;h3&gt;
  
  
  Is It Real, or a Lucky Single Roll?
&lt;/h3&gt;

&lt;p&gt;Before publishing a jump of that magnitude, I repeated the full C and E run on the multi-hop set the next day — same 15 questions, same corpus, same models, retrieval/generation/evaluation pipeline run from scratch, with its own independent evaluation job.&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;Correctness&lt;/th&gt;
&lt;th&gt;Completeness&lt;/th&gt;
&lt;th&gt;Faithfulness&lt;/th&gt;
&lt;th&gt;Helpfulness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C, run 1&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;td&gt;0.550&lt;/td&gt;
&lt;td&gt;0.778&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C, run 2&lt;/td&gt;
&lt;td&gt;0.467&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;td&gt;0.583&lt;/td&gt;
&lt;td&gt;0.767&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E, run 1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.883&lt;/td&gt;
&lt;td&gt;0.917&lt;/td&gt;
&lt;td&gt;0.911&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E, run 2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.867&lt;/td&gt;
&lt;td&gt;0.867&lt;/td&gt;
&lt;td&gt;0.922&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result holds. E's Correctness came out exactly 1.00 across both independent runs — it's not an artifact of a lucky roll. C stays low in both, with a 0.033 difference between runs that's normal judge noise, not a trend. The gap between C and E in Correctness is stable; the other three metrics vary a bit more run-to-run (up to 0.05) but never cross the gap between configurations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;ProTip #5&lt;/strong&gt;: A jump from 0.50 to 1.00 in a single run of 15 questions is exactly the kind of number that invites suspicion of an artifact — an average inflated by discarded records, a miscounted &lt;code&gt;null_result&lt;/code&gt;. Before publishing any jump that large, repeat the full run from scratch. If it doesn't hold, it was noise. If it holds, as it does here, you have real evidence worth reporting with confidence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The honest reading, with all its caveats:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;N=15 is still a small set.&lt;/strong&gt; The pattern repeats across 2 runs, but that doesn't replace a larger N. The direction of the effect is well-supported; the exact values for Completeness, Faithfulness, and Helpfulness carry more run-to-run noise than Correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The planner and generator share a model&lt;/strong&gt;, the deviation already declared above. I can't rule out that part of the gain comes from Sonnet 4.6, as planner, "knowing better what it's going to be asked next" — a self-consistency effect, not necessarily better decomposition reasoning in the abstract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't close the problem entirely.&lt;/strong&gt; b09 and b11 remain unresolved even with the most capable planner I could test — the gain is real, but partial: 2 of 5 questions fixed, 1 partial, 2 unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clean comparison (Nova Premier) isn't available&lt;/strong&gt; due to its EOL date. This run can't isolate "larger planner" from "planner that's also the generator" — that separation would have required a third large model without that role conflict.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The conclusion the evidence does support:&lt;/strong&gt; a CUSTOM planner larger than Haiku decomposes more often (8/15 vs. 3/15 on the multi-hop set) and, when it decomposes toward the right side, moves the metrics substantially — something Haiku never managed to demonstrate. What I can't cleanly separate, with this design, is how much of that gain is "more capable model" and how much is "same model that already generates the final answer".&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed Since April (And What Didn't)
&lt;/h2&gt;

&lt;p&gt;One claim from April is now outdated, and two assumptions I started this benchmark with turned out to be wrong. All three get corrected here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Sonnet 4.6 is now on the judge allowlist&lt;/strong&gt; for RAG/KB evaluation specifically. In April I had to use Nova Pro because of that restriction, so I left the corresponding update note in the &lt;a href="https://gerardo.dev/en/chunking-benchmark.html" rel="noopener noreferrer"&gt;original article&lt;/a&gt; instead of leaving that fact hanging there. Even so, I kept Nova Pro as the judge in this benchmark: the design calls for a cross-family judge as an independent fairness control, to avoid the self-preference bias known in LLM-as-judge setups when the judge shares a family with the generator (which is still Sonnet 4.6 in A, B, and C). The new availability doesn't change that design logic.&lt;/p&gt;

&lt;p&gt;The other two are corrections to my own prior research, not to what was published in April.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;foundationModelType&lt;/code&gt; accepts &lt;code&gt;CUSTOM&lt;/code&gt; and &lt;code&gt;MANAGED&lt;/code&gt;&lt;/strong&gt;, with &lt;code&gt;MANAGED&lt;/code&gt; as the default. This wasn't clear in the first pass over the public documentation I did before running anything — only &lt;code&gt;CUSTOM&lt;/code&gt; examples were documented. Confirmed both against the updated API reference and against the real botocore shape in the account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first boto3/botocore version that supports &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; is 1.43.32&lt;/strong&gt; — that release shipped the operation and "Managed Knowledge Bases" together, in the same API commit. My machine's global Python had an older version; I had to isolate the project in its own virtualenv.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Table
&lt;/h2&gt;

&lt;p&gt;With the data in hand, here's what I'd tell someone evaluating this migration today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production RAG with mostly single-hop questions&lt;/td&gt;
&lt;td&gt;Migrate to Managed KB with simple &lt;code&gt;Retrieve&lt;/code&gt;. Quality ties a well-tuned manual configuration, and you save yourself the vector store operations burden.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your corpus has large files that break manual chunking (&lt;code&gt;SEMANTIC&lt;/code&gt;, &lt;code&gt;NONE&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Managed KB solves this directly via Smart Parsing. It's the clearest, least debatable win in this benchmark.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequent multi-hop or comparative questions&lt;/td&gt;
&lt;td&gt;Don't turn on &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; with the default planner without testing it against your own corpus first. In my case it was worse than not using it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're going to use &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; regardless&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;foundationModelType=CUSTOM&lt;/code&gt; with an explicit model from day one — never leave the default in production without having validated it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're going to pick a CUSTOM planner&lt;/td&gt;
&lt;td&gt;Try a large model before settling for a small one. In my benchmark, Haiku 4.5 decomposed rarely and poorly; Sonnet 4.6 decomposed almost three times as often and fixed 2 of 5 problem questions, with 1 more partially. The added latency and cost of a larger planner can be justified if your use case has genuinely multi-hop questions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're going to use the same model as planner and generator&lt;/td&gt;
&lt;td&gt;It's valid, and it worked well in my benchmark — but don't assume the result isolates "large planner" from "planner that knows what it's about to be asked." If you have a way to test a large model &lt;em&gt;different&lt;/em&gt; from your generator, do it before generalizing the recommendation to your architecture.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Still Open
&lt;/h2&gt;

&lt;p&gt;This benchmark, like April's, has a deliberately bounded scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolating planner size from role collision.&lt;/strong&gt; Config E uses Sonnet 4.6 as both planner and generator. The clean separation would have been a large model &lt;em&gt;different&lt;/em&gt; from both — Nova Premier was the only option AWS cited, and it fell out of scope due to its end-of-life date (September 14, 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Larger N on the multi-hop set.&lt;/strong&gt; 15 questions, even replicated across two runs, is still too few for serious confidence intervals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-KB routing&lt;/strong&gt;, &lt;code&gt;AgenticRetrieveStream&lt;/code&gt;'s other core capability that I didn't touch here — this benchmark used a single Managed KB across all configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per query compared across planners.&lt;/strong&gt; This benchmark measures quality and latency, not cost. And the question left open is fairly concrete: if a large planner decomposes almost three times as often, how much does the per-query bill go up against the gain in Correctness? That gets answered by measuring planning tokens per query, not by looking at aggregate account spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spanish-language corpus&lt;/strong&gt;, out of scope just like in April.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solving b09 and b11&lt;/strong&gt;, the two cross-document questions that neither the managed planner nor any CUSTOM planner I tested fully solved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you replicate this in your own account and the managed planner does decompose on your corpus, I want to know — it would be evidence that the behavior depends more on content type than my data suggests.&lt;/p&gt;

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

&lt;p&gt;Managed Knowledge Base isn't a quality regression. On simple retrieval, it does the same as my manual April configuration, and it effortlessly solves the ingestion problem that broke two of the five chunking strategies in April. If your only question is "do I lose quality by giving up chunking control?", the answer with this data is no.&lt;/p&gt;

&lt;p&gt;But the "agentic" in &lt;code&gt;AgenticRetrieveStream&lt;/code&gt;'s name promises something the default planner doesn't deliver, and a poorly chosen planner doesn't guarantee either. It's not misleading marketing —AWS documents early-exit behavior as normal, and its own code examples never use the default— but it is an easy trap to fall into if you turn the feature on expecting it to "be smarter" without explicitly choosing who does the reasoning. With the managed planner, it was a measured regression. With a small planner, it was real but poorly aimed decomposition. With a large planner —even though it shared a model with my generator, a cleanup I couldn't fully isolate due to Nova Premier's retirement— Correctness on multi-hop questions doubled, sustained across two independent runs.&lt;/p&gt;

&lt;p&gt;The real advice, if you're working with this in production, isn't "turn on the agentic retriever." It's "turn on the agentic retriever, pick your own planner —and probably a bigger one than you feel like trying first—, and &lt;strong&gt;measure&lt;/strong&gt; on your own corpus whether it decomposes the questions that genuinely need it." Because the range I found on the same corpus, with the same multi-hop question, went from "worse than not using it" to "twice as good as using it without thinking," depending on who was planning.&lt;/p&gt;

&lt;p&gt;The full benchmark code —Terraform, numbered scripts, the 15 multi-hop questions with their cross-verification, and the BYOI datasets— is at &lt;a href="https://github.com/codecr/mkb-vs-chunking" rel="noopener noreferrer"&gt;github.com/codecr/mkb-vs-chunking&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚀 &lt;strong&gt;Final Pro Tip&lt;/strong&gt;: Before turning on any "agentic" feature in production, ask yourself which model is making the planning decisions and whether that model is the one you chose or the one the service chose for you. And if you're the one choosing, don't settle for the first "small, fast" model the documentation suggests — in my benchmark, the difference between a small planner and a large one was bigger than the difference between having a planner and not having one at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want the full context, the &lt;a href="https://gerardo.dev/en/chunking-benchmark.html" rel="noopener noreferrer"&gt;original chunking benchmark&lt;/a&gt; is still the reference for manual configurations. And if what you want is to expose a Managed KB as an MCP tool for your agents, that path goes through &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-gateway-target.html" rel="noopener noreferrer"&gt;AgentCore Gateway as a Knowledge Base target&lt;/a&gt; —the connector exposes &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; and &lt;code&gt;Retrieve&lt;/code&gt; as MCP tools— and my article on &lt;a href="https://gerardo.dev/en/bedrock-mcp.html" rel="noopener noreferrer"&gt;Amazon Bedrock + MCP&lt;/a&gt; covers the protocol context.&lt;/p&gt;

&lt;p&gt;See you in the next article! If you replicated this or tried a different planner, tell me about it in the comments — with this particular finding, the more eyes that verify it, the better. 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  Official Resources 📚
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/blogs/machine-learning/agentic-retrieval-for-amazon-bedrock-managed-knowledge-base/" rel="noopener noreferrer"&gt;Blog: Agentic retrieval for Amazon Bedrock Managed Knowledge Base&lt;/a&gt; — source of the MuSiQue numbers and the foundationModelType=CUSTOM examples&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/build-enterprise-search-for-agents-with-amazon-bedrock-managed-knowledge-base/" rel="noopener noreferrer"&gt;Blog: Build enterprise search for agents with Amazon Bedrock Managed Knowledge Base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_AgenticRetrieveStream.html" rel="noopener noreferrer"&gt;API Reference: AgenticRetrieveStream&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-agentic-retrieve.html" rel="noopener noreferrer"&gt;Use agentic retrieval to query a knowledge base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-build-managed.html" rel="noopener noreferrer"&gt;Build a managed knowledge base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-create.html" rel="noopener noreferrer"&gt;Create a managed knowledge base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-customize-ingestion.html" rel="noopener noreferrer"&gt;Customize ingestion for a data source (Smart Parsing by default)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html" rel="noopener noreferrer"&gt;Retrieve data and generate AI responses with Amazon Bedrock Knowledge Bases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-gateway-target.html" rel="noopener noreferrer"&gt;Connect to your knowledge base through AgentCore Gateway (KB as an MCP tool)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-managed-kb.html" rel="noopener noreferrer"&gt;Amazon Bedrock Managed Knowledge Bases as Connector Target&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation-kb.html" rel="noopener noreferrer"&gt;Evaluate the performance of RAG sources using Amazon Bedrock Evaluations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-evaluation-create-randg.html" rel="noopener noreferrer"&gt;Creating a retrieve-and-generate RAG evaluation job (BYOI path)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/03/amazon-bedrock-rag-evaluation-generally-available/" rel="noopener noreferrer"&gt;Announcement: Amazon Bedrock RAG Evaluation generally available (March 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/evaluate-models-or-rag-systems-using-amazon-bedrock-evaluations-now-generally-available/" rel="noopener noreferrer"&gt;Blog: Evaluate models or RAG systems using Amazon Bedrock Evaluations — now GA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>agentcore</category>
      <category>knowledgebases</category>
    </item>
    <item>
      <title>How to build a tiny 1.5B text-to-SQL model that beats a 7B</title>
      <dc:creator>Pradhyumna Holla</dc:creator>
      <pubDate>Fri, 04 Sep 2026 07:10:20 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-build-a-tiny-15b-text-to-sql-model-that-beats-a-7b-298</link>
      <guid>https://dev.to/aws-builders/how-to-build-a-tiny-15b-text-to-sql-model-that-beats-a-7b-298</guid>
      <description>&lt;p&gt;I wanted to build something with an LLM using my own hands. Not wire an API into a wrapper, but take a base model, train it, measure it, break it, and serve it over HTTP. Why? Because I wanted to get my hands dirty working with an LLM, making and breaking things. &lt;/p&gt;

&lt;p&gt;Text to SQL fits exactly. You ask a question in plain English, the system writes SQL, and you can run the query to find out whether it was right. Correctness is whether the rows match the reference query's rows against the real database, which is a fact rather than a judgement. It also has a mature benchmark in &lt;a href="https://huggingface.co/datasets/xlangai/spider" rel="noopener noreferrer"&gt;Spider&lt;/a&gt;, 10,000 human written questions over 200 real SQLite databases, split so the test databases never appear in training.&lt;/p&gt;

&lt;p&gt;So I built it. Qwen2.5-0.5B as the base, LoRA adapters, one g5.xlarge with an NVIDIA A10G, about a dollar an hour. Continued pretraining on SQL text, then supervised fine-tuning on question and query pairs, then reinforcement learning with GRPO on top. The dashboards looked great. Reward climbing to 1.0, loss dropping cleanly through every stage.&lt;/p&gt;

&lt;p&gt;It scored 6.4%. Comically bad levels of accuracy.&lt;/p&gt;

&lt;p&gt;I didn't find that out for a while, because there was no held out evaluation. Every number I had came from data the model had trained on. The untrained base model scored 17.4%, so three stages of training had made things worse than no training at all.&lt;/p&gt;

&lt;p&gt;The fix wasn't a better model. It was building the thing that could tell me I was wrong, an evaluation harness that runs both queries against the real database and compares the rows that come back.&lt;/p&gt;

&lt;p&gt;Then I rebuilt. 6.4% to 44.6% on real schemas, then 49.7% with a proper RL reward, all still at 0.5B. Only then did I switch to Qwen2.5-Coder-1.5B, which landed at 68.1%. Sampling eight answers per question and returning whichever result most of them agreed on took it to 71.5%, against 71.2% for Qwen2.5-Coder-7B-Instruct.&lt;/p&gt;

&lt;p&gt;The 7B is still better at one attempt, 71.2% to 68.1%. The 1.5B gets there by answering eight times and voting, trading compute for the gap.&lt;/p&gt;

&lt;p&gt;That trade is the point though, and the point of this entire blog, and it's the one a lot of people are making now. Take a small model, aim it at one task, and build enough system around it that it beats something far larger at that one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;The first version was three training stages run back to back on &lt;code&gt;Qwen2.5-0.5B&lt;/code&gt;, a half billion param open model from Alibaba, all of them using LoRA(&lt;a href="https://arxiv.org/abs/2106.09685" rel="noopener noreferrer"&gt;Low-Rank Adaptation&lt;/a&gt;), which freezes the model's real weights and trains a tiny fraction of new, lightweight parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continued PreTraining&lt;/strong&gt;: keep training the base model on raw SQL text so it gets used to the shape of the language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supervised FineTuning&lt;/strong&gt;: show it thousands of question and correct-query pairs and have it imitate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reinforcement Learning with GRPO&lt;/strong&gt;: Group Relative Policy Optimization. The model writes several answers to the same question, each is scored, and it gets pushed toward whichever ones scored above that group's average. It learns from its own attempts rather than from copied answers.&lt;/p&gt;

&lt;p&gt;Here is what those runs reported:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Loss&lt;/th&gt;
&lt;th&gt;Data actually seen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Continued PreTraining&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;2.46 to 0.81&lt;/td&gt;
&lt;td&gt;~2.7 passes over the corpus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supervised FineTuning&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;3.26 to 0.51&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.10 of one pass&lt;/strong&gt;, about 8,000 of 78,577 examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reinforcement Learning&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;reward 0.8 to 1.0&lt;/td&gt;
&lt;td&gt;5,000 rows of the same training set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You have to understand that this was a learning project and my approach was flawed, so a few things went wrong. Every curve went the right way, and none of them meant anything, for three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data taught the wrong task&lt;/strong&gt;: &lt;code&gt;sql-create-context&lt;/code&gt; hands the model a schema already trimmed to exactly the columns the answer needs, so it never has to work out which table matters. Working that out is the entire job. It also saw only a tenth of that data, because the run was configured by step count rather than by passes over it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reward could not teach anything&lt;/strong&gt;: a formatting reward worth 1.0 fired for essentially every answer, and an exact string match reward worth 2.0 fired for almost none, so a group of four answers usually scored &lt;code&gt;[1.0, 1.0, 1.0, 1.0]&lt;/code&gt;. GRPO scores each answer against its group's average, so when they all score the same the update is zero. The metric that tracks this hit 1.00 by step 160, meaning a large share of those 300 steps did nothing at all. And exact string matching is the wrong test anyway. It scores &lt;code&gt;age&amp;gt;56&lt;/code&gt; as zero against &lt;code&gt;age &amp;gt; 56&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There was no held out evaluation&lt;/strong&gt;: the RL stage ran on a slice of the same data the model had been finetuned on, so every number I had measured how well it remembered its own training set.&lt;/p&gt;

&lt;p&gt;That last one is the real defect and the other two follow from it. Bad data and a broken reward are ordinary mistakes. What made them expensive is that nothing in the system could report them. The dashboards were green throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding, still at 0.5B
&lt;/h2&gt;

&lt;p&gt;I stayed on the half billion param model for all of this. Not because it was good, but because a full fine tune plus a full evaluation there is about two GPU hours. When you are wrong five times in a row, that matters more than the ceiling does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix the data first
&lt;/h3&gt;

&lt;p&gt;The old training set handed the model schemas already trimmed to the columns the answer needed. The rebuild uses Spider with the real, complete database schemas, so the model has to find the right table among all the wrong ones. Three other things changed at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loss is computed on the answer only&lt;/strong&gt;: A training example is the schema, then the question, then the correct query. With full schemas the schema part is roughly 75% of the tokens. If you score the model on reproducing the whole example, most of the training signal is teaching it to generate database schemas, which nobody asked for. Masking the prompt means every bit of the signal lands on the query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two full passes over the data instead of a tenth of one&lt;/strong&gt;: An epoch is one complete pass through the training set. The old run had covered 0.10 of one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A learning rate suited to what was actually being trained&lt;/strong&gt;: The learning rate controls how big each update is. The old value was appropriate for nudging a model that already knew the task. LoRA starts its extra weights from scratch, so it needs a larger one.&lt;/p&gt;

&lt;p&gt;That is the single biggest change in the project. &lt;strong&gt;6.4% to 44.6%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One thing I did not expect. Validation loss picked the wrong checkpoint. Epoch 2 looked slightly worse than epoch 1 on both validation loss and token accuracy, and it was &lt;strong&gt;3.9 points better&lt;/strong&gt; on real execution accuracy. Loss is a proxy for a proxy. Run the metric you actually care about, on held out data, at every checkpoint.&lt;/p&gt;

&lt;p&gt;In hindsight the old setup is a bit embarrassing, but every one of these fixes came from the failures, not from knowing better. Something that's true for most things that we do, not just machine learning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then fix the reward
&lt;/h3&gt;

&lt;p&gt;Now that the harness existed, the reward could use it. Instead of a black and white, all or nothing test, the reward is a ladder with partial credit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Reward&lt;/th&gt;
&lt;th&gt;What it buys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No SQL, or it will not parse&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;It parses as SQL&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;td&gt;syntax is a solved sub-problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The database accepted and ran it&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;the tables and columns actually exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The rows match the reference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point of the middle rungs is the zero gradient problem from before. A group of four answers where none is fully correct now scores something like &lt;code&gt;[0.2, 0.5, 0.5, 0.2]&lt;/code&gt; instead of &lt;code&gt;[1.0, 1.0, 1.0, 1.0]&lt;/code&gt;. There is real disagreement inside the group, so there is a real update.&lt;/p&gt;

&lt;p&gt;The 0.5 rung is aimed at one specific failure. Going into this, 88% of the model's remaining mistakes were invented column and table names. That rung pays for nothing except the columns existing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;44.6% to 49.7%.&lt;/strong&gt; The mechanism moved in exactly the way the ladder was designed to move it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unknown_column   647 → 458   (down 29%)
unknown_table     66 →   7   (down 89%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How I know that gain is real
&lt;/h3&gt;

&lt;p&gt;Two models answering the same 2,147 questions share most of their answers, and the shared ones tell you nothing. Only the questions where they disagree carry information. &lt;strong&gt;McNemar's test&lt;/strong&gt; looks at exactly those. If the two models were equally good, the disagreements should split roughly evenly between them.&lt;/p&gt;

&lt;p&gt;They did not. RL fixed 251 questions and broke 142. The test says a split that lopsided happens by chance with probability below 0.001.&lt;/p&gt;

&lt;p&gt;The fixed and broken counts matter as much as the test. The headline is plus 5.1 points, but that is a &lt;strong&gt;net&lt;/strong&gt;. It is not a clean sweep, and reporting it as one would hide 142 regressions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three library defaults that broke my reward
&lt;/h3&gt;

&lt;p&gt;None of these throw an error. They just quietly change what the reward means, and nothing tells you it happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reward scaling flattens the ladder&lt;/strong&gt;: the library divides each group's scores by how spread out that group was. The order of the rungs survives, the spacing does not, so a group whose best answer was genuinely correct ends up pushing exactly as hard as one whose best answer merely parsed. The ladder becomes a ranking. Switching that off keeps the 7.5 times gap between correct and parsed intact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reference model is not the one you think&lt;/strong&gt;: RL usually penalizes the model for drifting too far from a reference version of itself, measured as KL divergence. Under LoRA that reference is your adapter switched off, which is the raw base model, not your finetuned checkpoint. So the penalty was measuring how far supervised finetuning had already moved, not what RL was doing. I set it to zero rather than pull the model back toward something I had deliberately trained it away from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Row caps must not be able to change a verdict&lt;/strong&gt;: the reward stops reading rows once it has one more than the correct answer has. Anything longer could not have matched anyway, so the cap can never turn a right answer into a wrong one. It only stops a hallucinated cross join from materializing a million rows inside the training loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reward Hacking
&lt;/h3&gt;

&lt;p&gt;Reward hacking is when the model finds a way to score well without doing the task. It is usually discovered from a reward curve that climbs while the benchmark stays flat, which is to say after the GPU hours are gone.&lt;/p&gt;

&lt;p&gt;So before spending the GPU hours, I attacked my own reward. I wrote eleven policies that never look at the question at all, and measured how much reward each one could collect. &lt;code&gt;SELECT 1&lt;/code&gt; always parses and never matches. &lt;code&gt;SELECT * FROM &amp;lt;first table&amp;gt;&lt;/code&gt; always runs. &lt;code&gt;WHERE 1 = 0&lt;/code&gt; goes after the empty result loophole. Degenerate cross joins test the row cap and the parser guards.&lt;/p&gt;

&lt;p&gt;The best of them collected &lt;strong&gt;26.8%&lt;/strong&gt; of what a real answer gets, and got &lt;strong&gt;1.8%&lt;/strong&gt; of questions outright, which is the empty answer slice and exactly where I expected it to score. So the reward is not easy to cheat, and that is a number now rather than an assumption.&lt;/p&gt;

&lt;p&gt;This probe is also what caught the reward scaling default above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching it to reason, and why that made it worse
&lt;/h2&gt;

&lt;p&gt;The original training data told the model to think before answering. The problem was that the "thinking" was &lt;strong&gt;one hardcoded sentence repeated across all 5,378 examples&lt;/strong&gt;. As scratch paper it is blank. It carries no information about the specific question, so all the model learns is to recite a preamble before answering exactly as it would have anyway.&lt;/p&gt;

&lt;p&gt;The proper version of this is &lt;strong&gt;rejection sampling&lt;/strong&gt;, sometimes called STaR(Self-Taught Reasoner). Take a larger model that can actually reason, have it solve each of your training questions eight times while showing its work, then &lt;strong&gt;run every attempt against the database and keep only the ones whose SQL was correct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That filter is the entire technique. A large model is wrong confidently and fluently, and training a small model on fluent wrong reasoning is worse than not training it at all. The harness from earlier is what makes the filter possible, which is the third job that one component ended up doing.&lt;/p&gt;

&lt;p&gt;It worked as a data pipeline. 4,823 verified traces, covering 90.8% of the training questions, with a median of 114 words of reasoning that named real tables and real columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it made the model worse. 44.6% down to 41.8%.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The reasoning crowded out the SQL
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Training data&lt;/th&gt;
&lt;th&gt;Tokens the model writes&lt;/th&gt;
&lt;th&gt;Of which are SQL&lt;/th&gt;
&lt;th&gt;Share of the signal landing on the SQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Canned sentence&lt;/td&gt;
&lt;td&gt;62.6&lt;/td&gt;
&lt;td&gt;33.6&lt;/td&gt;
&lt;td&gt;53.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real traces&lt;/td&gt;
&lt;td&gt;195.1&lt;/td&gt;
&lt;td&gt;31.1&lt;/td&gt;
&lt;td&gt;15.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Training only teaches the model on the tokens it writes. With the canned sentence, it wrote about 63 tokens per answer and 34 of them were the SQL, so more than half its practice was on the query itself. With real traces it wrote about 195 tokens and the SQL was still only 31 of them. The query had not got any longer. It was just buried under 114 words of reasoning.&lt;/p&gt;

&lt;p&gt;So the model went from spending half its practice on the query to spending a sixth of it. Same number of passes over the data, same learning rate, 3.4 times less practice on the only part that gets marked. Running it for twice as many passes gave that practice back, and it came out at &lt;strong&gt;47.1%&lt;/strong&gt;, above where it started.&lt;/p&gt;

&lt;h3&gt;
  
  
  The gains didn't really stack
&lt;/h3&gt;

&lt;p&gt;Running reinforcement learning on top of the trace trained model was a wash. Plus 0.1 points, 179 questions fixed and 177 broken. Statistically that is nothing.&lt;/p&gt;

&lt;p&gt;The reason is that both interventions attack the same failure. Traces help the model name real tables and columns because the reasoning it copied named real tables and columns. The execution reward helps with exactly the same thing, and it is better at it. So the traces had already done a partial version of RL's job and left it nothing to work on.&lt;/p&gt;

&lt;p&gt;The best 0.5B model in the project stayed the straightforward one, supervised finetuning plus RL, at &lt;strong&gt;49.7%&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is in the post
&lt;/h3&gt;

&lt;p&gt;It cost real time and it did not produce the headline number, so the temptation is to leave it out. Two reasons not to.&lt;/p&gt;

&lt;p&gt;First, a negative result with a measured cause is more useful than a positive one without. "Reasoning traces did not help" is folklore. "Reasoning traces diluted the training signal on the answer by 3.4 times, and doubling the passes over the data fixed it" is something you can act on.&lt;/p&gt;

&lt;p&gt;Second, it is a clean example of the thing this whole project is about. The pipeline ran, the data was verified, the traces were genuinely good, and the model got worse. Nothing about that was visible from the training curves. The only reason I know any of it is that there was a held out number to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did I just need a bigger model?
&lt;/h2&gt;

&lt;p&gt;At 49.7% the 0.5B model had run out of road with this training setup. The failure breakdown said the remaining errors were not the kind a better reward fixes, so I changed the base model to &lt;code&gt;Qwen2.5-Coder-1.5B&lt;/code&gt;. Three times the parameters, and pretrained on code rather than general text, which should help with a task whose output is code.&lt;/p&gt;

&lt;p&gt;Same pipeline, nothing else changed. &lt;strong&gt;67.9%&lt;/strong&gt;, and 68.1% after RL.&lt;/p&gt;

&lt;p&gt;That is a large jump and it invites the obvious objection. Maybe none of the previous work mattered and I just needed a bigger model. That is an empirical question, so I answered it with an experiment instead of an opinion. Run the &lt;strong&gt;old&lt;/strong&gt; pipeline and the &lt;strong&gt;new&lt;/strong&gt; one on &lt;strong&gt;both&lt;/strong&gt; base models. Four runs, every combination.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pipeline&lt;/th&gt;
&lt;th&gt;0.5B&lt;/th&gt;
&lt;th&gt;1.5B&lt;/th&gt;
&lt;th&gt;What scale bought&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Old&lt;/td&gt;
&lt;td&gt;4.6%&lt;/td&gt;
&lt;td&gt;46.9%&lt;/td&gt;
&lt;td&gt;+42.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;44.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+23.2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;What technique bought&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;+40.1&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;+20.9&lt;/em&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;Both effects are large and both hold up statistically. Technique at 1.5B fixed 559 questions and broke 110. Scale under the new pipeline fixed 577 and broke 78.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both matter, and they eat into each other&lt;/strong&gt;: Technique is worth 20.9 points even on a bigger, code pretrained base, so the work is not just compensating for a weak model. But that is half what it was worth at 0.5B, because the bigger model had already solved some of what the technique was fixing.&lt;/p&gt;

&lt;p&gt;The corners of the table show the same thing. Measured on its own, technique is worth 40.1 points and the bigger model is worth 42.4, so you would expect 82 from doing both. Going from the worst combination to the best actually gets you 63.3, from 4.6% to 67.9%. The missing 19 points are questions both changes would have fixed on their own, and you only get paid for those once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two things that qualify that table
&lt;/h3&gt;

&lt;p&gt;The old pipeline at 0.5B also carried a tokenizer bug that stopped generation from terminating properly, while the 1.5B version of it used a working stop token. So the +42.4 in the top row is partly "a bug was also fixed". The two numbers I actually rely on are the ones underneath it, where everything else is held fixed.&lt;/p&gt;

&lt;p&gt;And the scale column is not purely scale. The new base is both larger &lt;strong&gt;and&lt;/strong&gt; code pretrained, so those two things are tangled together in the +23.2 and I cannot separate them without a fourth base model I did not train. The technique column is clean, since that is the same base with two pipelines. The scale column should be read as "bigger and code pretrained", not "bigger".&lt;/p&gt;

&lt;p&gt;Worth saying that the old pipeline was set up to lose as gracefully as possible. Its destructive pretraining stage was left out, the library that corrupted the vocabulary was left out, the working stop token was used. It is the strongest fair version of the thing it loses to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reinforcement learning stopped helping
&lt;/h3&gt;

&lt;p&gt;Same reward, same configuration, run on the new base.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RL on the 0.5B base:  +5.1 points   251 fixed, 142 broken   p &amp;lt; 0.001
RL on the 1.5B base:  +0.2 points   122 fixed, 117 broken   not significant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Not significant" here means the fixed and broken counts are close enough that the difference is indistinguishable from chance. 122 against 117 is a coin flip.&lt;/p&gt;

&lt;p&gt;The strange part is that the mechanism still worked perfectly. Hallucinated columns fell 23%. The share of queries the database would actually run rose from 88.2% to 90.6%. Syntax errors dropped from 5 to 2. &lt;strong&gt;Everything improved except the score.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reason is that the reward pays for executability, and executability had stopped being the bottleneck. At 0.5B, invented columns dominated the failures, so fixing them moved the number. At 1.5B the model already runs 90% of its queries, and what remains are queries that execute perfectly and answer the wrong question. This reward cannot see that. A query that runs and returns wrong rows scores 0.5, exactly the same as one that runs and returns wrong rows for a completely different reason.&lt;/p&gt;

&lt;p&gt;Two diagnostics from that run are worth carrying to any RL project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;56% of training groups produced no gradient at all&lt;/strong&gt;: That is the original v0 failure arriving for the opposite reason. Back then the rollouts were uniformly wrong, so nothing separated them. Now they are uniformly right. &lt;strong&gt;Reinforcement learning gets harder as your policy gets better&lt;/strong&gt;, because it runs on disagreement inside a group, and a good model disagrees with itself less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training reward rose 24% while held out accuracy moved 0.2 points&lt;/strong&gt;: With entropy falling steadily the whole time. Entropy here is how spread out the model's choices are, and it falling means the model is getting more confident and less varied. That is precisely the shape of the dashboards that started this whole project. The only difference is that this time there was a held out number sitting next to it saying the gain was not real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting more out of the weights we already have
&lt;/h2&gt;

&lt;p&gt;Everything so far changes the model's weights. At 68.1% it is worth asking what the remaining 32% actually looks like, because the answer decides what you can do about it without training anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,462  correct                                        68.1%
  483  runs perfectly, answers the wrong question     22.5%   no error exists
  202  the database rejects it outright                9.4%   an error message exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two completely different populations. The second one has a signal attached, because when SQLite refuses a query it tells you why, in words, like &lt;code&gt;no such column: customer_name&lt;/code&gt;. The first one has nothing. The query runs, rows come back, and everything looks fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letting it retry, which barely works
&lt;/h3&gt;

&lt;p&gt;The obvious move for the 202 is to show the model its own error and let it try again, up to three rounds.&lt;/p&gt;

&lt;p&gt;Two things make that a measurement rather than a demo. The loop &lt;strong&gt;never sees the correct answer&lt;/strong&gt;. Its only stopping signal is whether the database accepted the query, which is all that exists at inference time in the real world. And the rounds are &lt;strong&gt;batched rather than per question&lt;/strong&gt;. Round one generates all 2,147, and only the roughly 200 rejections go into round two. Looping question by question is about 50 times slower for an identical answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It bought 1.1 points.&lt;/strong&gt; I had predicted 4 and written that down beforehand.&lt;/p&gt;

&lt;p&gt;The reason is the useful part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;202  rejected on the first attempt
 63  became something the database would run    31% of rejections
 23  were actually correct                      37% of those repairs
 40  moved from "database refused it" to "runs fine, wrong rows"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two multiplications there, not one. &lt;strong&gt;An error message tells you that you are wrong. It never tells you what is right.&lt;/strong&gt; So the model fixes the complaint rather than the answer, and two thirds of its repairs migrate from the visible failure bucket into the invisible one.&lt;/p&gt;

&lt;p&gt;At 0.5B it does essentially nothing. 500 rejections, 12 repaired. A weak model told "no such column: X" still has no idea which column does exist.&lt;/p&gt;

&lt;p&gt;I do not report a significance test for retry, on purpose. The test works by weighing how many questions got fixed against how many got broken, and retry cannot break any. A query the database rejected was already wrong, so replacing it either helps or changes nothing. The broken count was zero in all four runs, which I checked rather than assumed. With nothing on the losing side, the test would call any gain significant at all, even a gain of one question. What actually means something here is the size of the gain against what it costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voting, which works, and works where retry cannot
&lt;/h3&gt;

&lt;p&gt;The 483 queries that run cleanly and return wrong rows produce no error, so retry is structurally blind to them. But they do &lt;strong&gt;disagree with each other&lt;/strong&gt;, and disagreement is an indicator/signal.&lt;/p&gt;

&lt;p&gt;So instead of taking the model's single most likely answer, sample eight different ones, run all eight, group them &lt;strong&gt;by the rows they return rather than by the text of the query&lt;/strong&gt;, and answer with the largest group. Two queries written completely differently that return identical rows are probably both right. A hallucinated one usually returns something nobody else got.&lt;/p&gt;

&lt;p&gt;The grouping has to go through the same comparison function the benchmark uses, not a hash of the rows. That comparison ignores column order, so &lt;code&gt;SELECT age, name&lt;/code&gt; and &lt;code&gt;SELECT name, age&lt;/code&gt; count as one answer. Hashing would be faster and would split exactly the groups voting exists to merge.&lt;/p&gt;

&lt;p&gt;Two things are built in as checks rather than assumed. The first of the eight candidates is always the model's ordinary single answer, the one it would have given without voting, so setting &lt;strong&gt;k to 1&lt;/strong&gt; has to reproduce the plain score exactly. If it ever doesn't, something is wired wrong and I find out straight away. And voting at any k only looks at the first k candidates, so generating 16 once lets me score every budget from 1 to 16 without generating anything again.&lt;/p&gt;

&lt;h4&gt;
  
  
  The trap
&lt;/h4&gt;

&lt;p&gt;Every query that returns nothing looks identical to every other query that returns nothing. A broken filter, a made up condition matching no rows, and a genuinely empty answer all land in the same group and vote together. In every other group, agreement means several differently written queries arrived at the same rows, which is real evidence. In the empty group it only means several queries failed to return anything, which is not. Three people shrugging is not a consensus.&lt;/p&gt;

&lt;p&gt;So an empty group loses to any group that came back with actual rows. Both halves of that trade are measured. It is worth &lt;strong&gt;1.3 points&lt;/strong&gt;, and it costs 6 questions where the correct answer really was empty.&lt;/p&gt;

&lt;h4&gt;
  
  
  What the curve says
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;k&lt;/th&gt;
&lt;th&gt;vote@k&lt;/th&gt;
&lt;th&gt;pass@k, the ceiling&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Gain from doubling&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;68.1%&lt;/td&gt;
&lt;td&gt;68.1%&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;69.2%&lt;/td&gt;
&lt;td&gt;70.1%&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;+1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;70.6%&lt;/td&gt;
&lt;td&gt;72.8%&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;+1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;71.7%&lt;/td&gt;
&lt;td&gt;75.4%&lt;/td&gt;
&lt;td&gt;3.7&lt;/td&gt;
&lt;td&gt;+1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;vote@k&lt;/code&gt; is what the system actually answers with. &lt;code&gt;pass@k&lt;/code&gt; is whether &lt;strong&gt;any&lt;/strong&gt; of the k candidates was correct, which you can only know by checking the answer key afterwards. It is the ceiling a perfect chooser could reach.&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%2Fozf60jz2yqvcazcsgffe.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%2Fozf60jz2yqvcazcsgffe.png" alt="vote@k against pass@k" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Voting saturates and the ceiling does not. Each doubling buys 1.2, then 1.4, then 1.1, then 0.4. The last doubling returns a third of the one before it. But &lt;code&gt;pass@k&lt;/code&gt; keeps climbing, so &lt;strong&gt;the gap between them widens from 0.0 to 4.8 points&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That gap is right answers the model generated and threw away. At 16 samples the bottleneck is no longer producing a correct query. It is recognising the one already in hand. Measured at k = 8 alone this looks like a technique with room left in it, and it is not.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where the gain comes from
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;greedy result  →  vote result
    correct    →  correct      1452
  ran, wrong   →  ran, wrong    453
   rejected    →  rejected      106
   rejected    →  correct        45
   rejected    →  ran, wrong     45
  ran, wrong   →  correct        38   ← invisible to retry
    correct    →  ran, wrong      8

fixed 83, broke 8    p &amp;lt; 0.001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Significance testing is legitimate here, because voting can and did break things. Eight regressions makes the comparison two directional again.&lt;/p&gt;

&lt;p&gt;There are two separate results in that table. Voting reaches the bucket retry cannot see at all, 38 questions. And it &lt;strong&gt;beat retry on retry's own ground&lt;/strong&gt;, repairing 45 rejected queries against retry's 23. Seven more samples turns out to be a better repair mechanism than one error message, which is worth knowing before building anything cleverer than sampling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why voting survives on a weak model and retry doesn't
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              retry    voting
1.5B          +1.1      +3.5
0.5B          +0.2      +3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Retry needs a model good enough to act on feedback. Voting only needs one that is right sometimes&lt;/strong&gt;, and then fishes that answer out. The second is a far weaker requirement, which is why voting survives at a model size where retry collapses entirely.&lt;/p&gt;

&lt;p&gt;That asymmetry is only visible because both techniques were measured at both sizes. Told as a 1.5B story, it would have been thrown away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reliability check that costs nothing
&lt;/h2&gt;

&lt;p&gt;Voting requires running eight or sixteen candidates anyway. Once you have done that, you already have something else for free, and I think it is the most useful output of the whole project. &lt;strong&gt;How much did the candidates agree with each other?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agreement across 16 samples&lt;/th&gt;
&lt;th&gt;Questions&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;How often correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;All 16 agree&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,405&lt;/td&gt;
&lt;td&gt;65.4%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;85.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 to 15 agree&lt;/td&gt;
&lt;td&gt;274&lt;/td&gt;
&lt;td&gt;12.8%&lt;/td&gt;
&lt;td&gt;60.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 to 11 agree&lt;/td&gt;
&lt;td&gt;377&lt;/td&gt;
&lt;td&gt;17.6%&lt;/td&gt;
&lt;td&gt;46.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing ran at all&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;td&gt;0.0%&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%2Ffeyiofr5thu3k8fb4yxe.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%2Ffeyiofr5thu3k8fb4yxe.png" alt="Accuracy at every level of self agreement" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the model agrees with itself it is right 86% of the time, and that covers two thirds of all questions. When it does not, it is close to a coin flip(46.2%).&lt;/p&gt;

&lt;p&gt;In a product that distinction is worth more than the 3.5 points voting adds to the score. It is the difference between a system that silently returns a wrong number and one that can say "I am not confident about this one, check it". A wrong SQL answer does not look wrong. It looks like a number in a cell.&lt;/p&gt;

&lt;h3&gt;
  
  
  The group that only appears at sixteen samples
&lt;/h3&gt;

&lt;p&gt;91 questions where not one of sixteen attempts produced a query the database would run. Not a wrong answer among them, because there was never an answer at all. Accuracy there is 0.0%, and it is 0.0% by definition rather than by bad luck.&lt;/p&gt;

&lt;p&gt;At eight samples these hide inside the low agreement band, indistinguishable from questions the model merely found hard. At sixteen they separate cleanly, and that separation is the whole value of them. The service can recognize these before it answers rather than after. Saying nothing on 4.2% of questions costs a great deal less than being confidently wrong on them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three things that keep it honest
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Each band is scored on its own members&lt;/strong&gt;: An earlier version of this table lumped 7-of-8 agreement in with 6-of-8 and reported the pair as 62.8%. Scored separately, 7-of-8 was 62.8% and 6-of-8 was 47.9%. Lumping them made the weaker half look 15 points better than it was, and anyone trusting that number would have trusted answers they should not have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bands are deliberately wide&lt;/strong&gt;: The middle of the chart is a mess. 5-of-16 scores 33% while 1-of-16 scores 55%, but those are only 27 and 33 questions each, so the gap between them is chance rather than anything real. The order is wrong too, with less agreement sometimes scoring better than more. Splitting that middle into finer bands would be reporting a precision that is not there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One answer gets no confidence at all&lt;/strong&gt;: If the model only answered once, there is nothing to compare it against, so the service returns "unmeasured" instead of a number. A confidence figure nobody has checked is worse than no figure.&lt;/p&gt;

&lt;p&gt;The same pattern shows up on the 0.5B model, just lower, with unanimous answers correct 80.7% of the time instead of 85.9%. That is what makes this a property of the approach rather than a fluke of one particular model.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is left is a choosing problem
&lt;/h3&gt;

&lt;p&gt;The vote vs ceiling gap from the last section has a specific shape. &lt;strong&gt;Voting picks a wrong answer while holding a correct one 3.6% of the time.&lt;/strong&gt; That looked like the cheapest remaining win in the project. No training, no GPU, the candidates are already sitting on disk.&lt;/p&gt;

&lt;p&gt;So I looked at the failures before building anything. &lt;strong&gt;87% of them are cases where the wrong answer won six votes to two.&lt;/strong&gt; The model is confidently and consistently wrong, and no rule based on counting votes can override a 6-2 majority.&lt;/p&gt;

&lt;p&gt;I built two smarter selectors anyway and measured them at +0.1 and −0.1 points. Dead end, and worth the hour it took to prove rather than the week it would have taken to build. Closing that gap needs a model trained to score candidates against the question, not a tiebreak rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is the part I would keep
&lt;/h3&gt;

&lt;p&gt;If I had to throw away everything in this project except one idea, it would be this one. Accuracy tells you how often a system is right on average. It tells a user nothing about the answer currently on their screen.&lt;/p&gt;

&lt;p&gt;Self agreement tells you about &lt;em&gt;this&lt;/em&gt; answer, not the average one, it costs nothing once you are sampling, it needs no extra model, and it works well enough to act on. The more valuable output of a system like this is not being right more often. It is knowing when to shut up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that would matter in production
&lt;/h2&gt;

&lt;p&gt;Every score so far has handed the model the complete, correct schema for the exact database the question is about. Ask about students, get the student database. Nothing else on screen.&lt;/p&gt;

&lt;p&gt;No real deployment works like that. A company's warehouse has hundreds or thousands of tables, nobody knows in advance which database a question belongs to, and the whole thing does not remotely fit in a model's context window. Something has to search the schema first, pick out the handful of tables that look relevant, and pass only those to the model. If that search picks wrong, the model never had a chance.&lt;/p&gt;

&lt;p&gt;This section takes the gift away and measures what it was worth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a test that can measure anything
&lt;/h3&gt;

&lt;p&gt;My first design was wrong, and the reason is worth a paragraph.&lt;/p&gt;

&lt;p&gt;Spider's databases are tiny. The median one has &lt;strong&gt;4 tables and 19 columns&lt;/strong&gt;, and writing it into a prompt as &lt;code&gt;CREATE TABLE&lt;/code&gt; statements costs about 102 tokens. Searching for the right table among four is not a search problem. Any method scores near perfect and you learn nothing.&lt;/p&gt;

&lt;p&gt;The obvious fix is to throw all 206 databases into one big pile and search that. This fails badly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;125 table names appear in more than one database
436 of 1,053 tables affected  (41%)

customers  → 22 databases      student → 12
addresses  → 15                staff   → 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 22 different tables called &lt;code&gt;customers&lt;/code&gt; in that pile, belonging to 22 unrelated databases. A question asking about customers genuinely does not say which one it means. &lt;strong&gt;No search method can resolve that&lt;/strong&gt;, however good, because the information needed to resolve it is not in the question. A test built this way would measure an impossible task and then blame the retriever for failing it.&lt;/p&gt;

&lt;p&gt;So I built a &lt;strong&gt;collision free&lt;/strong&gt; pool instead. Take databases one at a time, in order of how many benchmark questions they carry, and skip any database whose table names clash with one already taken. That leaves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;81 databases, &lt;strong&gt;300 tables&lt;/strong&gt;, 1,427 columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,457&lt;/strong&gt; of the 2,147 test questions&lt;/li&gt;
&lt;li&gt;No two tables anywhere in the pool sharing a name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of those 300 tables, only 107 belong to databases that carry any questions. The other &lt;strong&gt;193 are pure noise&lt;/strong&gt;, sitting there to be wrong answers.&lt;/p&gt;

&lt;p&gt;And the pool genuinely does not fit. Written out as &lt;code&gt;CREATE TABLE&lt;/code&gt; statements it comes to &lt;strong&gt;8,262 tokens, against the 3,072 token prompt limit&lt;/strong&gt; these models were evaluated under. Nearly three times over. Search is not an optimization here, it is the only way the prompt fits at all.&lt;/p&gt;

&lt;p&gt;One thing to carry through the rest of this section: every number below is over &lt;strong&gt;1,457 questions, not 2,147&lt;/strong&gt;. Putting a retrieval number next to a number from earlier in this post is comparing two different tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two ways to search
&lt;/h3&gt;

&lt;p&gt;Each table becomes one short searchable document, just its name and its column names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;student: id, name, age, dept_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search query is &lt;strong&gt;the question text and nothing else&lt;/strong&gt;. Never the database name. That is deliberate and important: if the retriever knew which database a question came from, it could just look up that database's tables directly, and every number here would be measuring an answer key rather than a search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt; is keyword matching, the classic search engine approach. A table scores higher when more of the question's words appear in it, and rare words count for more than common ones. Matching the word "hangar" tells you a lot, matching "the" tells you nothing.&lt;/p&gt;

&lt;p&gt;One detail carries a surprising amount of weight. Schema names are written like &lt;code&gt;city_code&lt;/code&gt; or &lt;code&gt;singerID&lt;/code&gt;, and people ask questions using words like "city" and "singer". A normal tokenizer treats &lt;code&gt;city_code&lt;/code&gt; as one indivisible thing, so a question about cities can never match it. Mine splits on both &lt;code&gt;snake_case&lt;/code&gt; and &lt;code&gt;camelCase&lt;/code&gt; and keeps the joined form as well, so &lt;code&gt;city_code&lt;/code&gt; becomes &lt;code&gt;city_code&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt;. Bridging that gap between English and identifiers is most of what this job actually is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense retrieval&lt;/strong&gt; matches on meaning instead of words. A small embedding model turns each table description into a list of numbers, positioned so that texts meaning similar things sit near each other. Do the same to the question, and the nearest tables win. It can match "how many people are enrolled" to a &lt;code&gt;student&lt;/code&gt; table without either phrase sharing a single word with the other.&lt;/p&gt;

&lt;p&gt;Both methods return the top &lt;strong&gt;k&lt;/strong&gt; tables, where k is just how many you decide to hand the model. Here is how often each finds what the question needs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;k&lt;/th&gt;
&lt;th&gt;BM25 recall&lt;/th&gt;
&lt;th&gt;BM25 coverage&lt;/th&gt;
&lt;th&gt;Dense recall&lt;/th&gt;
&lt;th&gt;Dense coverage&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;33.9%&lt;/td&gt;
&lt;td&gt;25.1%&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;44.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;63.9%&lt;/td&gt;
&lt;td&gt;53.8%&lt;/td&gt;
&lt;td&gt;88.1%&lt;/td&gt;
&lt;td&gt;79.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;73.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;86.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;78.6%&lt;/td&gt;
&lt;td&gt;70.7%&lt;/td&gt;
&lt;td&gt;95.1%&lt;/td&gt;
&lt;td&gt;90.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The column that matters is &lt;strong&gt;coverage&lt;/strong&gt;, not recall, and the difference between them is not a technicality.&lt;/p&gt;

&lt;p&gt;Recall is the share of needed tables that were found. Coverage is the share of questions where &lt;strong&gt;every&lt;/strong&gt; needed table was found. Say a question needs to join two tables and the search finds one of them. Recall calls that 50%, a decent partial score. But the question is exactly as unanswerable as if the search had found nothing at all, because you cannot write half a join. Coverage scores it zero, which is the truth.&lt;/p&gt;

&lt;p&gt;Dense wins clearly, which is not surprising given that questions are English and schemas are identifiers. BM25 is in here as the honest baseline, since it is free and needs no GPU, and if it had come close that would have been worth knowing.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it costs end to end
&lt;/h3&gt;

&lt;p&gt;Now run the actual model on what each search method hands it. Four conditions, identical in every respect except which schema text goes into the prompt, all over the same 1,457 questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oracle&lt;/strong&gt; is a cheat. It shows the model exactly the tables the correct answer uses and nothing else. Not achievable in reality, since knowing which tables the answer needs is most of the problem. It is here as a ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gold&lt;/strong&gt; is the whole correct database, which is what every number earlier in this post used. Re-measured here on the smaller question set so it compares fairly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dense&lt;/strong&gt; and &lt;strong&gt;BM25&lt;/strong&gt; are the real conditions, picking 10 tables out of 300.&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;Condition&lt;/th&gt;
&lt;th&gt;Schema shown&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Queries that run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Oracle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;exactly the tables the answer uses&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;92.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gold&lt;/td&gt;
&lt;td&gt;the whole correct database&lt;/td&gt;
&lt;td&gt;63.5%&lt;/td&gt;
&lt;td&gt;89.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dense, top 10&lt;/td&gt;
&lt;td&gt;10 tables out of 300&lt;/td&gt;
&lt;td&gt;45.2%&lt;/td&gt;
&lt;td&gt;66.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25, top 10&lt;/td&gt;
&lt;td&gt;10 tables out of 300&lt;/td&gt;
&lt;td&gt;37.1%&lt;/td&gt;
&lt;td&gt;57.5%&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%2Fzz88bz5y92kbahz1uik7.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%2Fzz88bz5y92kbahz1uik7.png" alt="All eight retrieval conditions" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two findings, and I predicted neither.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Showing less schema helps, if it is the right schema&lt;/strong&gt;: Oracle beats gold by 4.9 points, and gold is only showing tables from the correct database. Every irrelevant table costs something, even a related one.&lt;/p&gt;

&lt;p&gt;That is the exact opposite of what was true in training, where trimmed schemas were actively harmful because the model never learned to pick a table out of a crowd. Training on clutter is necessary. Having no clutter at question time is pure upside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most of the loss is distraction, not absence&lt;/strong&gt;: This is the one that surprised me. Split the dense results by whether the search actually found everything the question needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;all needed tables present   1,257 questions (86.3%)   accuracy 51.9%
at least one missing          200 questions (13.7%)   accuracy  3.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The missing group behaves exactly as you would expect. 3.0% is effectively zero, and those questions were unanswerable the moment the search missed. Fine.&lt;/p&gt;

&lt;p&gt;But look at the other group. &lt;strong&gt;Every table the question needed was sitting right there in the prompt, and accuracy was still only 51.9% against oracle's 68.4%.&lt;/strong&gt; Sixteen and a half points destroyed purely by the nine irrelevant tables sitting next to the right ones. The share of queries the database will even run drops from 92.1% to 66.8%, because the model keeps reaching for a plausible looking table that belongs to some completely unrelated database.&lt;/p&gt;

&lt;p&gt;Stacking those two costs together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oracle                             68.4%
  minus distraction  16.5 points →  51.9%   right tables present, plus nine more
  minus absence       6.7 points →  45.2%   13.7% of questions miss a table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Distraction costs two and a half times what absence does.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had predicted 58 to 61% before running this, by reasoning that accuracy would be roughly coverage multiplied by the baseline. In other words, I assumed that finding the tables was the whole problem, and that once found, the model would perform as it always had. The gap between that prediction and the real 45.2% is exactly the distraction cost, which my mental model of the system had no room for at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving fewer tables does not help
&lt;/h3&gt;

&lt;p&gt;If distraction is the dominant cost, then handing the model fewer tables should reduce it. I wrote that prediction down before testing it. Six more runs, both search methods at three different values of k, say it is wrong.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;k&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Accuracy when covered&lt;/th&gt;
&lt;th&gt;Accuracy when missing&lt;/th&gt;
&lt;th&gt;Accuracy overall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dense&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;79.2%&lt;/td&gt;
&lt;td&gt;55.6%&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;44.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dense&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;86.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dense&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;90.3%&lt;/td&gt;
&lt;td&gt;49.4%&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;td&gt;44.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;53.8%&lt;/td&gt;
&lt;td&gt;63.6%&lt;/td&gt;
&lt;td&gt;2.2%&lt;/td&gt;
&lt;td&gt;35.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;64.1%&lt;/td&gt;
&lt;td&gt;57.1%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;37.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;70.7%&lt;/td&gt;
&lt;td&gt;54.1%&lt;/td&gt;
&lt;td&gt;2.6%&lt;/td&gt;
&lt;td&gt;39.0%&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%2Fgagaaho5j0wn17uhxbcv.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%2Fgagaaho5j0wn17uhxbcv.png" alt="Coverage rising while covered accuracy falls" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at the dense rows first. Overall accuracy reads 44.3, 45.2, 44.8 across a fourfold change in how many tables get retrieved. Flat enough that you would reasonably conclude k does not matter much.&lt;/p&gt;

&lt;p&gt;That conclusion would be wrong, and the last two columns show why. &lt;strong&gt;Two opposing forces are cancelling out.&lt;/strong&gt; Coverage climbs from 79% to 90%, because searching wider finds the right tables more often. Accuracy on questions that were already covered falls from 56% to 49%, because every extra table is another distraction. One goes up, the other goes down, and the total sits still.&lt;/p&gt;

&lt;p&gt;BM25 starts so starved of recall, only 54% coverage at k=5, that the coverage gain outweighs everything else and its overall number climbs from 35.3 to 39.0. Its covered accuracy falls on exactly the same schedule though, 64% to 57% to 54%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether raising k appears to help depends entirely on whether your search method still had recall left to gain.&lt;/strong&gt; It never depends on the distraction going away, which got worse in every single condition tested.&lt;/p&gt;

&lt;p&gt;So the honest reading is narrower than "retrieve less". The number of tables you retrieve is not a dial worth tuning. It trades coverage against distraction at roughly one for one, and the 16.5 point distraction penalty survives every setting I tried. What would actually move this is making the model itself robust to irrelevant tables, by training it on cluttered schemas instead of clean ones. That is a training change, not a search change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to run
&lt;/h2&gt;

&lt;p&gt;A project can report 68.1% accuracy and say nothing at all about whether an answer takes 200 milliseconds or 30 seconds. So the last piece is an HTTP service, and a benchmark of it.&lt;/p&gt;

&lt;p&gt;The service &lt;strong&gt;reuses the voting and retry code directly&lt;/strong&gt; rather than reimplementing it. That is the point. A second copy of the clustering or selection logic could drift away from the numbers those techniques were measured at, and nothing would tell you. A request returns the SQL, the rows, a confidence band from the agreement table, and a timing breakdown.&lt;/p&gt;

&lt;p&gt;Measured on one A10G handling &lt;strong&gt;one request at a time&lt;/strong&gt;, which is what a service does, unlike the evaluation harness that batches sixteen together. Over the full test split, so the accuracy column is real evidence rather than a spot check.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Queries/sec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greedy&lt;/td&gt;
&lt;td&gt;67.9%&lt;/td&gt;
&lt;td&gt;2,652 ms&lt;/td&gt;
&lt;td&gt;5,100 ms&lt;/td&gt;
&lt;td&gt;7,162 ms&lt;/td&gt;
&lt;td&gt;3,088 ms&lt;/td&gt;
&lt;td&gt;0.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vote, 8 samples&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;71.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5,650 ms&lt;/td&gt;
&lt;td&gt;10,789 ms&lt;/td&gt;
&lt;td&gt;15,272 ms&lt;/td&gt;
&lt;td&gt;6,522 ms&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry, 3 rounds&lt;/td&gt;
&lt;td&gt;68.9%&lt;/td&gt;
&lt;td&gt;2,714 ms&lt;/td&gt;
&lt;td&gt;9,739 ms&lt;/td&gt;
&lt;td&gt;16,042 ms&lt;/td&gt;
&lt;td&gt;3,777 ms&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;p50 is the median request. p95 is the slowest one in twenty. p99 is the slowest one in a hundred.&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%2Fzf7zyt76h0radvmarv36.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%2Fzf7zyt76h0radvmarv36.png" alt="Latency by percentile for all three modes" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That accuracy column is doing quiet work. It reproduces the harness from a completely different code path with different batching, giving +3.4 for voting and +1.0 for retry against the harness's +3.5 and +1.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tail is the finding
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Retry's median is indistinguishable from plain greedy&lt;/strong&gt;, 2,714 ms against 2,652, because most requests never enter the loop at all. Every bit of its cost sits in the tail.&lt;/p&gt;

&lt;p&gt;Its mean of 3,777 ms hides that from both directions simultaneously. It is 40% above a median that describes most requests, and 76% below a p99 that describes the requests a user actually complains about. It describes neither.&lt;/p&gt;

&lt;p&gt;Voting is the opposite shape. It costs 2.1 times more at the median, but its p99 is only 2.7 times its own median, so what you measure in staging is what you get in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mean says retry is cheaper. The tail says voting is more predictable.&lt;/strong&gt; Users experience the tail.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dmjvgomlqvp1oplr8vo.gif" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dmjvgomlqvp1oplr8vo.gif" alt="Three questions answered live against a database the model has never seen" width="759" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last one is the whole system running. Three questions against a test database, answered by the service. The first two are correct and unanimous. The third asks for distinct types of planes owned by pilots, and returns four plausible plane names from the wrong table. Nothing in the SQL or the output reveals that. The only sign is one sample out of eight disagreeing, which drops the confidence badge from high to medium.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it ran on
&lt;/h2&gt;

&lt;p&gt;All of this is one &lt;code&gt;g5.xlarge&lt;/code&gt; in us-east-1. A single NVIDIA A10G with 24GB, four vCPUs, $1.006 an hour on demand. The three serving benchmarks above took ~1.8, ~4 and ~2.5 hours respectively, which is most of a working day of GPU time spent purely on measuring something I had already built.&lt;/p&gt;

&lt;p&gt;That ratio is the honest summary of the project. Far more compute went into finding out whether things worked than into making them work.&lt;/p&gt;

&lt;p&gt;Four pieces of infrastructure earned their place:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic shutdown when idle&lt;/strong&gt;: A cron job checks GPU utilization every minute and shuts the box down after 30 minutes below 5%. A forgotten &lt;code&gt;g5.xlarge&lt;/code&gt; is about $170 a week, and this is the single highest value cost control in the project.&lt;/p&gt;

&lt;p&gt;But the first version measured the wrong thing. &lt;strong&gt;GPU utilization alone is not "is this machine in use".&lt;/strong&gt; An evaluation run spends real minutes loading weights, building prompts, executing SQL against SQLite and scoring results, all at 0% GPU. That script shut down a live run twice before I fixed it. Idleness is now graded. If a process is still holding GPU memory, or a human is still logged in, the limit becomes 180 minutes instead of 30. Not immunity, because "forgot to log out" must not cost $170 a week either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rescuing work from spot instances&lt;/strong&gt;: Spot instances are 50 to 60% cheaper, and AWS can reclaim them with a two minute warning delivered through the instance metadata service. Reclaiming means terminate, not stop, so the disk goes with it. A service polls for that warning and syncs checkpoints to S3 when it arrives. Two minutes is enough to do one thing quickly and not enough to do anything clever.&lt;/p&gt;

&lt;p&gt;It is not the primary mechanism though. A missed notice or a kernel panic gives no warning at all, so checkpoints also go to S3 on every save. Resuming is not restarting the same machine, it is a fresh machine pulling the last checkpoint from S3 and carrying on from that step, which I tested by killing a run and deleting its disk.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One thing to note is that I never actually ran on spot. AWS took forever to process my quota request and by the time it came through, most of the project was already done. So this is built and tested but not battle worn. It is a genuinely good feature and half the price is half the price, so if you use it, let me know how it holds up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never mirror a checkpoint directory to S3.&lt;/strong&gt; The tidy way to stop S3 filling up with old checkpoints is &lt;code&gt;aws s3 sync --delete&lt;/code&gt;, which mirrors the local directory. It is also one fresh instance away from deleting the only copy of a run, because a new machine's checkpoint directory is empty and mirroring an empty directory deletes everything. Old checkpoints are about 110MB each. Letting them pile up is far cheaper than the failure they prevent.&lt;/p&gt;

&lt;p&gt;Also, I originally set everything up in us-east-2, because GPU instances ran about 30% cheaper there. Then I found out I could not launch an instance at all.&lt;/p&gt;

&lt;p&gt;AWS caps how many of each instance type you can run, per region, and those caps start at zero for GPU instances until you ask for an increase. Mine in us-east-2 was zero and stayed zero, so the cheaper price was for something I was not allowed to buy. Everything moved to us-east-1, where I already had approval for a few. Check your limit in a region before you plan around its pricing.&lt;/p&gt;

&lt;p&gt;Also worth knowing: a stopped instance still charges you for its disk. Mine was about $16 a month for a 200GB volume doing nothing at all. If you are finished with a machine, terminate it rather than leaving it stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Something trained to pick the answer&lt;/strong&gt;: At sixteen tries the model writes a correct query for 77% of questions and picks it only 72% of the time. Those 4.8 points are right answers it already produced and threw away. I tried two hand written rules for choosing better and they scored +0.1 and −0.1, so this needs a model trained to judge candidates against the question, not another rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training on messy schemas&lt;/strong&gt;: The model learned on tidy, correct database descriptions, then gets handed cluttered ones full of irrelevant tables and loses 16.5 points to the clutter. That is fixable in training, by showing it messy schemas while it learns, rather than in the search step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faster serving&lt;/strong&gt;: 2.7 seconds for a typical question is slow, and the service currently handles one request at a time. Processing several at once on the GPU is the standard fix and the single biggest weakness in these numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not multi turn reinforcement learning&lt;/strong&gt;, which was my original plan. The idea was to train the model on the whole back and forth of the retry loop rather than on single answers. Two things killed it. Only 37% of the queries it repairs end up correct, so there is not much there to learn from. And at 1.5B, over half of training groups already teach the model nothing, because all its attempts agree with each other and it needs disagreement to learn. Training on longer conversations makes that worse.&lt;/p&gt;

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

&lt;p&gt;The headline is that a 1.5B model matched a 7B one. That is true, and it is also the least interesting thing here.&lt;/p&gt;

&lt;p&gt;The model did not get good because it got bigger. It went from 6.4% to 49.7% at half a billion parameters, before I touched the model size at all, and every one of those gains came from something outside the weights. A way to check answers by running them. A reward that had a gradient in it. Training data that did not hide the hard part. Then, once the model was as good as that size allowed, the same measuring tools were pointed at a bigger base and the gains transferred intact.&lt;/p&gt;

&lt;p&gt;And the last 3.4 points, the ones that actually drew level with the 7B, came from no training whatsoever. Sample eight answers, run all eight, keep whichever result most of them agreed on. That is a system decision, not a model decision.&lt;/p&gt;

&lt;p&gt;Look at what the pieces actually are, stripped of SQL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A mechanical check for whether an answer is right&lt;/li&gt;
&lt;li&gt;A reward built out of that same check, so evaluation and training agree by construction&lt;/li&gt;
&lt;li&gt;Sampling several answers and using agreement between them to choose&lt;/li&gt;
&lt;li&gt;Using that same agreement to know when to keep quiet&lt;/li&gt;
&lt;li&gt;Retrieval, when the context needed does not fit&lt;/li&gt;
&lt;li&gt;Held out measurement on every claim, including the ones that came back negative&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the first is SQL specific, and only barely. Anywhere you can mechanically decide whether an output is correct, all six apply. Code that has to compile and pass tests. Structured extraction validated against a schema. Maths with a checker. An API call that either succeeds or does not. In each case you can build the same loop, and in each case a small model wrapped in it will go further than its parameter count suggests.&lt;/p&gt;

&lt;p&gt;This is why the small model + good system approach works, and why so many people are converging on it right now. You are not trying to beat a frontier model at everything. You are picking one task, building the infrastructure that knows what correct looks like, and letting a cheap model take as many attempts as it needs.&lt;/p&gt;

&lt;p&gt;The expensive part was never the model. It was building the thing that could tell me when I was wrong.&lt;/p&gt;

&lt;p&gt;Thank you for reading my blog, this was fun to build, and I learnt a lot of things too. Happy to receive your thoughts in the comments! Connect with me on &lt;a href="https://www.linkedin.com/in/pradhyumna-n-holla/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; and &lt;a href="https://x.com/PradHolla" rel="noopener noreferrer"&gt;X&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/PradHolla/SQL-Reasoning-LLM" rel="noopener noreferrer"&gt;SQL-Reasoning-LLM on GitHub&lt;/a&gt; — Code, with evaluation harness, analysis scripts that produced every chart, and the results files each number came from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The benchmark&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://yale-lily.github.io/spider" rel="noopener noreferrer"&gt;Spider&lt;/a&gt; — Benchmark, with the leaderboard and the databases.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/1809.08887" rel="noopener noreferrer"&gt;Spider paper&lt;/a&gt; — Yu et al., 2018, if you want the construction details and the difficulty splits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The techniques&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2402.03300" rel="noopener noreferrer"&gt;GRPO&lt;/a&gt; — introduced in the DeepSeekMath paper. Section 4 is where the group relative advantage comes from, which is the formula that dictates the whole reward design.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2203.11171" rel="noopener noreferrer"&gt;Self-consistency&lt;/a&gt; — Wang et al., the origin of "sample several answers and take the majority". Execution voting is this with the results as the thing being voted on rather than the text.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2203.14465" rel="noopener noreferrer"&gt;STaR&lt;/a&gt; — Zelikman et al., the rejection sampling approach behind the reasoning traces.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2106.09685" rel="noopener noreferrer"&gt;LoRA&lt;/a&gt; — Freezing the model's real weights, training a small set of extra ones. Why any of this fits on one GPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B" rel="noopener noreferrer"&gt;Qwen2.5-Coder-1.5B&lt;/a&gt; — the base model.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/trl" rel="noopener noreferrer"&gt;TRL&lt;/a&gt; — the library the GRPO trainer is built on. Worth reading the &lt;code&gt;GRPOConfig&lt;/code&gt; defaults carefully, since two of them silently changed the meaning of my reward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/ec2/instance-types/g5/" rel="noopener noreferrer"&gt;EC2 G5 Instances&lt;/a&gt; — High performance GPU based instances for graphics intensive applications and ML inference&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html" rel="noopener noreferrer"&gt;Spot instance interruption notices&lt;/a&gt; — the two minute warning, and how to poll for it from the instance metadata service. This is what the checkpoint rescue in this post is built on, and it is worth reading before you put a training run on spot rather than after.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>aws</category>
      <category>python</category>
    </item>
    <item>
      <title>What I left off the file portal on AWS — how far FSx for ONTAP features reach, and the work handed to a schedule (Part 6)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:59:30 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-i-left-off-the-file-portal-on-aws-how-far-fsx-for-ontap-features-reach-and-the-work-handed-hk8</link>
      <guid>https://dev.to/aws-builders/what-i-left-off-the-file-portal-on-aws-how-far-fsx-for-ontap-features-reach-and-the-work-handed-hk8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is the last part of the series. &lt;a href="https://dev.to/aws-builders/adding-a-file-portal-to-fsx-for-ontap-s3-access-points-choosing-between-amplify-gen2-and-887"&gt;Part 1&lt;/a&gt; built a file portal on Amazon FSx for NetApp ONTAP (FSx for ONTAP) S3 access points, &lt;a href="https://dev.to/aws-builders/embedding-storage-operations-into-a-file-portal-from-arpai-incident-response-to-regulatory-1oih"&gt;Part 2&lt;/a&gt; added ransomware response and WORM retention, &lt;a href="https://dev.to/aws-builders/embedding-ai-agents-into-a-file-portal-from-agentcore-mcp-to-multi-agent-teams-part-3-19m1"&gt;Part 3&lt;/a&gt; put AI agents on it, Part 4 delegated 182 storage operations, and Part 5 collected what the running cluster refused.&lt;/p&gt;

&lt;p&gt;This part is &lt;strong&gt;what was left off the screen&lt;/strong&gt;. That is the difference from last time: where Parts 4 and 5 were about operations you run from a screen, this one has two subjects. How much of ONTAP stays usable without going through the portal at all, and the operational work that was handed to a schedule instead of a button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this part does not cover&lt;/strong&gt;: performance figures (this measures whether things are possible and under what condition, not throughput), running SnapLock or Object Lock (irreversible, so not enabled without an instruction naming the retention), and actual tiering into a capacity pool (needs a different aggregate configuration, which was not on this path).&lt;/p&gt;




&lt;h2&gt;
  
  
  The reach of ONTAP features on this path
&lt;/h2&gt;

&lt;p&gt;When you read and write files over the S3 API through an FSx for ONTAP S3 access point, how much of ONTAP stays usable alongside it? You want qtrees to carve up directories, quotas to stop a tenant running away with the space, FlexClone to spin a verification copy. Does any of that sound like your environment?&lt;/p&gt;

&lt;p&gt;Start looking into it and NetApp's documentation gives you pause. &lt;a href="https://docs.netapp.com/us-en/ontap/s3-config/ontap-s3-interoperability-concept.html" rel="noopener noreferrer"&gt;ONTAP S3 interoperability&lt;/a&gt; records qtrees, quotas, FlexClone, and a volume clone of a FlexGroup containing ONTAP S3 buckets as &lt;strong&gt;not supported&lt;/strong&gt; — for the &lt;strong&gt;ONTAP S3 server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is where it stalls. The FSx for ONTAP S3 access point is an AWS mechanism, and what it attaches to is a &lt;strong&gt;volume&lt;/strong&gt;, not a bucket. Attaching one does stand up an ONTAP S3 server on the SVM, and I/O through the access point goes through ONTAP's S3 protocol stack. So the two are not unrelated. Does the same restriction appear?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I measured it. Up front: everything I measured worked.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ONTAP feature&lt;/th&gt;
&lt;th&gt;Result on the S3 access point path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qtree&lt;/td&gt;
&lt;td&gt;Can be created. Appears as an S3 prefix, and an object PUT into it lands inside the qtree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quota&lt;/td&gt;
&lt;td&gt;A tree quota refuses an S3 PUT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexClone (volume granularity)&lt;/td&gt;
&lt;td&gt;The clone can be made, and &lt;strong&gt;the clone itself can take an S3 access point&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexClone (file granularity)&lt;/td&gt;
&lt;td&gt;A file written over S3 can be cloned, and &lt;strong&gt;the clone is visible as an object through the access point&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexGroup volume&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;An S3 access point can be attached.&lt;/strong&gt; PUT / GET / LIST and multipart all work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clone of a FlexGroup&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The clone can be created&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The four items that table lists did not appear as restrictions on this path.&lt;/strong&gt; Reading that table as "not supported there, therefore not supported here" is wrong, and so is the reverse: a "supported" row is not evidence about this path either. &lt;strong&gt;The subjects differ, so nothing is known until it is measured.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What actually tripped me up was not in the table at all. Three things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;NTFS volume with a UNIX identity on an SVM with no CIFS server&lt;/strong&gt; attaches, reaches &lt;code&gt;AVAILABLE&lt;/code&gt;, and then refuses every data operation with &lt;code&gt;AccessDenied&lt;/code&gt;. &lt;strong&gt;&lt;code&gt;AVAILABLE&lt;/code&gt; says nothing about the health of the file system layer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A volume created on the ONTAP side takes &lt;strong&gt;ten to twenty minutes&lt;/strong&gt; to appear in the AWS API. The documentation says "several minutes"&lt;/li&gt;
&lt;li&gt;A volume that has &lt;strong&gt;once carried an S3 access point cannot be deleted from the ONTAP side&lt;/strong&gt;, even after every access point is detached. Only &lt;code&gt;aws fsx delete-volume&lt;/code&gt; removes it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is the walkthrough, and how the controls were set up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;2026-08-26 (UTC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP version&lt;/td&gt;
&lt;td&gt;9.18.1P3D1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment type / throughput&lt;/td&gt;
&lt;td&gt;SINGLE_AZ_1 / 128 MBps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test volumes&lt;/td&gt;
&lt;td&gt;UNIX security style 1 GiB × 2 (one with an access point, one as the control), NTFS 1 GiB × 1, FlexGroup 400 GiB × 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access point identity&lt;/td&gt;
&lt;td&gt;UNIX &lt;code&gt;root&lt;/code&gt;, internet origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup&lt;/td&gt;
&lt;td&gt;Throwaway volumes only. All deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on the identity&lt;/strong&gt;: UNIX &lt;code&gt;root&lt;/code&gt; was used to keep the measurement simple. Every request through an access point is authorized as that one file system identity, so specifying &lt;code&gt;root&lt;/code&gt; removes any narrowing by file permissions. &lt;strong&gt;This is not a recommended configuration.&lt;/strong&gt; Use a dedicated user with only the permissions the writes need, and split access points per purpose (&lt;code&gt;FileSystemIdentity&lt;/code&gt; cannot be changed after creation).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The distinction that matters most — different subjects
&lt;/h2&gt;

&lt;p&gt;The results below have to be read with these two kept apart.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Whose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ONTAP S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The S3 server with which ONTAP serves buckets. This is what NetApp's table is about&lt;/td&gt;
&lt;td&gt;NetApp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FSx for ONTAP S3 access point&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An AWS access point attached to an FSx for ONTAP &lt;strong&gt;volume&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They are not unrelated. Attaching one stands up an ONTAP S3 server on the SVM, and the I/O goes through ONTAP's S3 protocol stack (measured). &lt;strong&gt;That is exactly why NetApp's table is the right place to start.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Being the right place to start is not the same as being the conclusion. The target is a volume rather than a bucket, and the namespace and the authorization path both differ. &lt;strong&gt;"Not supported there, therefore not supported here" turns something unchecked into something checked.&lt;/strong&gt; The reverse reading is the same error.&lt;/p&gt;




&lt;h2&gt;
  
  
  The walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Qtree — usable
&lt;/h3&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create a qtree in the volume with an access point&lt;/td&gt;
&lt;td&gt;Succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control: the same qtree in the volume with no access point&lt;/td&gt;
&lt;td&gt;Succeeded. &lt;strong&gt;No difference&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-objects-v2 --delimiter /&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The qtree appears in CommonPrefixes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT into the qtree prefix&lt;/td&gt;
&lt;td&gt;Succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect the qtree from ONTAP&lt;/td&gt;
&lt;td&gt;The written file is inside the qtree directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;NetApp's "Qtrees: not supported" &lt;strong&gt;does not manifest as a refusal on this path.&lt;/strong&gt; The qtree is created, it is visible as a prefix in the S3 namespace, and an object written into that prefix lands inside it. Together with the quota result below, it behaves as a qtree.&lt;/p&gt;

&lt;p&gt;Incidentally, a volume with an access point attached carries an internal &lt;code&gt;____NTAP_S3_MAPPING&lt;/code&gt; directory at its root. It is visible over NFS and SMB, so anyone who inspects a collect-layer volume by hand will see it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quota — enforced against an S3 PUT
&lt;/h3&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set a tree quota on the qtree (space 1 MiB, files 10) and enable quotas on the volume&lt;/td&gt;
&lt;td&gt;Succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quota report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The rule is active, and &lt;strong&gt;the file written over S3 is counted in files used&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT 15 small objects&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;8 succeeded, 7 refused.&lt;/strong&gt; files used stopped at exactly 10/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the S3 client received on refusal&lt;/td&gt;
&lt;td&gt;HTTP 507 &lt;code&gt;InsufficientCapacity&lt;/code&gt; / &lt;code&gt;Maximum storage capacity of file system has been reached.&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control: raise the files limit 10 → 50 and re-PUT the refused key with the same body&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The control carries the conclusion. Raising the limit alone made the identical PUT succeed, so the refusal was the quota, not a capacity shortage and not a permissions artefact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That the response mis-describes its cause matters to the design.&lt;/strong&gt; The file system was not full; a qtree file-count quota was reached. An operator who sees a 507 and "maximum storage capacity of file system" will consider growing the file system. If you use quotas on this path, &lt;strong&gt;write down what that response actually means in the runbook.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  FlexClone — both volume and file granularity
&lt;/h3&gt;

&lt;p&gt;There are two granularities, and they behave differently.&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;File granularity&lt;/th&gt;
&lt;th&gt;Volume granularity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it lands&lt;/td&gt;
&lt;td&gt;The same volume. It appears as a new key on the same access point&lt;/td&gt;
&lt;td&gt;A separate volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization boundary&lt;/td&gt;
&lt;td&gt;Same as the parent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Its own access point, its own policy, its own identity&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time before it is usable&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;599 to 1,177 s measured, for a volume created through the ONTAP API to appear on the AWS side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible over S3&lt;/td&gt;
&lt;td&gt;Yes. &lt;code&gt;StorageClass=FSX_ONTAP&lt;/code&gt;, sha256 matches the source&lt;/td&gt;
&lt;td&gt;Yes. LIST / GET / PUT through the clone's own access point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At either granularity the clone shares data blocks with its parent. Cloning a 256 MiB file four times:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;th&gt;Logical&lt;/th&gt;
&lt;th&gt;Physical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just after one 256 MiB PUT&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;253,534,208 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After four file-granularity clones&lt;/td&gt;
&lt;td&gt;1,350,942,720 B&lt;/td&gt;
&lt;td&gt;277,200,896 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Logical grew to five copies, physical by about 23 MB. &lt;strong&gt;FSx for ONTAP bills provisioned SSD capacity, so there is no capacity to provision per duplicate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NetApp states that copies consume no storage except what is required for metadata until changes are written to the copy (&lt;a href="https://docs.netapp.com/us-en/ontap/concepts/flexclone-volumes-files-luns-concept.html" rel="noopener noreferrer"&gt;FlexClone volumes, files, and LUNs&lt;/a&gt;). The operation that ends the sharing is a &lt;strong&gt;split&lt;/strong&gt; (&lt;code&gt;volume clone split start&lt;/code&gt;), which I did not measure. From ONTAP 9.4, on AFF systems where the volume guarantee is &lt;code&gt;none&lt;/code&gt;, the documentation states that the split shares the physical blocks rather than copying data (&lt;a href="https://docs.netapp.com/us-en/ontap/volumes/split-flexclone-from-parent-task.html" rel="noopener noreferrer"&gt;splitting procedure&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A file-granularity clone gives no observable failure, however.&lt;/strong&gt; &lt;code&gt;POST /api/storage/file/clone&lt;/code&gt; returns 202 and a job UUID, but the UUID resolved to &lt;code&gt;404 entry doesn't exist&lt;/code&gt; and appeared in no job listing. The same &lt;code&gt;fsxadmin&lt;/code&gt; retrieves volume-create and volume-clone jobs as &lt;code&gt;state=success&lt;/code&gt;, so this is not a permissions problem. A call naming a destination directory that does not exist also returned 202 and created nothing. &lt;strong&gt;Judge the outcome by inspecting the destination file.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  FlexGroup — access point attachment
&lt;/h3&gt;

&lt;p&gt;Creation has conditions, learned by failing in order.&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create with default parameters&lt;/td&gt;
&lt;td&gt;Failed. &lt;code&gt;Volumes of this type must be at least 50GB&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry at 50 GiB&lt;/td&gt;
&lt;td&gt;Failed. &lt;code&gt;Aggregates not matching FabricPool requirements: aggr1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry with the aggregate named explicitly&lt;/td&gt;
&lt;td&gt;Failed. &lt;code&gt;Minimum size is "400GB"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 GiB, explicit aggregate, &lt;code&gt;tiering.policy=none&lt;/code&gt;, thin&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;FlexGroup volumes &lt;strong&gt;can&lt;/strong&gt; be created from the AWS Management Console, the AWS CLI and the Amazon FSx API (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/managing-volumes.html" rel="noopener noreferrer"&gt;Managing volumes&lt;/a&gt;). Naming the aggregate explicitly was required because this one was created through the ONTAP REST API; that constraint does not appear on the AWS-side path.&lt;/p&gt;

&lt;p&gt;What follows is the point: whether an access point on the AWS side attaches to a FlexGroup created on the ONTAP side.&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Appears on the FSx for ONTAP side as &lt;code&gt;VolumeStyle=FLEXGROUP&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;It does, with an &lt;code&gt;fsvol-&lt;/code&gt; identifier&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attach an S3 access point to the FlexGroup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded. &lt;code&gt;Lifecycle=AVAILABLE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT / GET / LIST&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded. The GET content matched&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 MiB multipart upload&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded. &lt;code&gt;StorageClass=FSX_ONTAP&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot the FlexGroup&lt;/td&gt;
&lt;td&gt;Succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clone that FlexGroup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded. &lt;code&gt;style=flexgroup&lt;/code&gt;, &lt;code&gt;is_flexclone=true&lt;/code&gt;, online&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the item NetApp's table records as "volume clone of the FlexGroup volume containing ONTAP S3 buckets: not supported".&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually failed on this path
&lt;/h2&gt;

&lt;p&gt;The constraints were not on NetApp's side of the table. Within what I measured, these three are the real traps.&lt;/p&gt;

&lt;h3&gt;
  
  
  A security-style and identity mismatch — failure after a successful attachment
&lt;/h3&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attach with an NTFS-security-style volume + UNIX identity (&lt;code&gt;root&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded. &lt;code&gt;Lifecycle=AVAILABLE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT through that access point&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Refused. &lt;code&gt;AccessDenied&lt;/code&gt;, body only &lt;code&gt;Access Denied&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control: same identity, same caller, UNIX volume&lt;/td&gt;
&lt;td&gt;PUT / GET / LIST all succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CIFS server on this SVM&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;None.&lt;/strong&gt; The UNIX-to-Windows mapping cannot resolve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AVAILABLE&lt;/code&gt; does not mean the file system layer is healthy.&lt;/strong&gt; Attachable and usable are different properties: IAM and the access point policy are both passed, and the file system layer refuses after that. The body is only &lt;code&gt;Access Denied&lt;/code&gt; and does not name the layer. A refusal at the identity layer says &lt;code&gt;no identity-based policy allows ...&lt;/code&gt; instead, so &lt;strong&gt;the difference in the body is the signal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trying WINDOWS identity on the same SVM does not even complete the attachment.&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attach with WINDOWS identity on an SVM with no CIFS server&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Failed. &lt;code&gt;did not stabilize&lt;/code&gt; (&lt;code&gt;NotStabilized&lt;/code&gt;), stack rolled back&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether an attachment survives the rollback&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;It does not.&lt;/strong&gt; Nothing was orphaned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The wait for something created in ONTAP to appear on the AWS side
&lt;/h3&gt;

&lt;p&gt;A volume created through the ONTAP API is not immediately in the AWS-side &lt;code&gt;describe-volumes&lt;/code&gt;. Without an &lt;code&gt;fsvol-&lt;/code&gt; identifier, neither &lt;code&gt;AWS::FSx::S3AccessPointAttachment&lt;/code&gt; nor &lt;code&gt;create-and-attach-s3-access-point&lt;/code&gt; can reference it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FlexGroup, polled every 20 s with no gaps&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Appeared at 599 s (about 10 min)&lt;/strong&gt;, with an &lt;code&gt;fsvol-&lt;/code&gt; identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A FlexClone volume, polled every 20 s with no gaps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Appeared at 1,177 s (about 19.6 min)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A separate run&lt;/td&gt;
&lt;td&gt;Still absent at 1,258 s (about 21 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The three observations disagree, so this is not an upper bound.&lt;/strong&gt; The order of magnitude is tens of minutes, which is not enough to bake a fixed wait into a design. AWS documents the following, and the measurement is longer than its "several minutes".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon FSx periodically syncs with ONTAP to ensure consistency. If you create or modify volumes using NetApp applications, it may take up to several minutes for these changes to be reflected in the AWS Management Console, AWS CLI, API and SDKs.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/managing-resources-ontap-apps.html" rel="noopener noreferrer"&gt;Managing FSx for ONTAP resources using NetApp applications&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;This delay has to be kept apart from absence.&lt;/strong&gt; In an observation of about 2.5 minutes, a volume created through the ONTAP API shows no &lt;code&gt;fsvol-&lt;/code&gt; identifier, and it would be easy to conclude from that that neither a clone nor a FlexGroup can take an access point. Wait, and it appears, and it attaches. 2.5 minutes sits inside the "a few minutes" AWS documents, so &lt;strong&gt;what is observable in that window is "not yet appeared".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It lags in the other direction too. Setting a junction path on the ONTAP side and attaching immediately fails with &lt;code&gt;Amazon FSx is unable to attach S3access point because the volume is not mounted.&lt;/code&gt; The AWS-side &lt;code&gt;JunctionPath&lt;/code&gt; was &lt;code&gt;None&lt;/code&gt; at that moment and still was two minutes later. Setting it with &lt;code&gt;aws fsx update-volume&lt;/code&gt; made it visible on the AWS side in about 40 seconds, and the attachment then succeeded. &lt;strong&gt;The error message was telling the truth.&lt;/strong&gt; What was wrong was my assumption that setting it in ONTAP made it visible to AWS.&lt;/p&gt;

&lt;h3&gt;
  
  
  A residual NAS bucket refusing volume deletion on the ONTAP side
&lt;/h3&gt;

&lt;p&gt;First, the terms. An S3 access point is an AWS-side mechanism and an object store NAS bucket is an ONTAP-side object, and &lt;strong&gt;the two are different things.&lt;/strong&gt; Attaching an access point creates a corresponding NAS bucket on the ONTAP side.&lt;/p&gt;

&lt;p&gt;What happened here is residue of the latter. &lt;strong&gt;After every access point was detached, the NAS bucket remained on the ONTAP side, and ONTAP refused to delete the volume because of it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot delete volume "..." in SVM "..." because it is associated with the following
object store NAS buckets: "amazon-fsx-&amp;lt;volume-id&amp;gt;"
&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;What was checked&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where the bucket name comes from&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;An exact match for the volume ID.&lt;/strong&gt; It is created per volume, not per access point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After deleting every access point&lt;/td&gt;
&lt;td&gt;Still refused, and still refused hours later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bringing the volume back online and mounted&lt;/td&gt;
&lt;td&gt;Still refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-attaching an access point and detaching it in the correct order&lt;/td&gt;
&lt;td&gt;Still refused. &lt;strong&gt;It is independent of the access point lifecycle&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws fsx delete-volume&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Succeeded.&lt;/strong&gt; Both the volume and the bucket went&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;NetApp's documented &lt;a href="https://docs.netapp.com/us-en/ontap/revert/remove-nas-bucket-task.html" rel="noopener noreferrer"&gt;procedure for removing a NAS bucket configuration&lt;/a&gt; uses &lt;code&gt;vserver object-store-server bucket delete&lt;/code&gt;, which &lt;strong&gt;could not be applied here because the target could not be listed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether this residue is by design or a defect is unconfirmed.&lt;/strong&gt; No AWS documentation I could find describes a NAS bucket outliving its access points and blocking a volume delete; the documented removal path is &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/delete-access-point.html" rel="noopener noreferrer"&gt;&lt;code&gt;detach-and-delete-s3-access-point&lt;/code&gt;&lt;/a&gt;. If you hit the same state, &lt;code&gt;delete-volume&lt;/code&gt; on the AWS side will complete the teardown, but ask support to establish the cause.&lt;/p&gt;

&lt;p&gt;Not being listed is not the same as not existing. Laid out by reader:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;How it shows this bucket / S3 server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP REST &lt;code&gt;/protocols/s3/buckets&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Does not list it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP CLI &lt;code&gt;vserver object-store-server bucket show&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Does not list it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP REST &lt;code&gt;/svm/svms&lt;/code&gt; (the &lt;code&gt;s3&lt;/code&gt; field)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reports&lt;/strong&gt; the S3 server as &lt;code&gt;enabled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP REST &lt;code&gt;/protocols/s3/services&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Does not list&lt;/strong&gt; the same SVM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Objects that AWS manages are hidden from the standard ONTAP S3 views.&lt;/strong&gt; I confirmed the bucket's absence across two readers, and both shared the same blind spot.&lt;/p&gt;

&lt;p&gt;If you have made clones, the teardown order needs one more step of care. &lt;code&gt;delete-volume&lt;/code&gt; returned &lt;code&gt;DELETING&lt;/code&gt; and then silently went back to &lt;code&gt;CREATED&lt;/code&gt; (observed twice). The reason appears only in &lt;code&gt;LifecycleTransitionReason&lt;/code&gt; on &lt;code&gt;describe-volumes&lt;/code&gt;: &lt;code&gt;Failed to delete volume because it has one or more clones.&lt;/code&gt; The clones had been deleted, and were sitting in ONTAP's &lt;strong&gt;volume recovery queue&lt;/strong&gt;. The parent kept &lt;code&gt;clone.has_flexclone&lt;/code&gt; at &lt;code&gt;true&lt;/code&gt;, and the AWS-side delete reads that flag and refuses. Purging the recovery queue cleared the flag and the same &lt;code&gt;delete-volume&lt;/code&gt; worked. &lt;strong&gt;That queue appears in neither the console nor the FSx for ONTAP API.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting it in a runbook
&lt;/h2&gt;

&lt;p&gt;Translated into operational terms, the measurements come to five points.&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;What to write in the runbook&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;
&lt;strong&gt;Match the security style to the identity.&lt;/strong&gt; UNIX identity for UNIX volumes, WINDOWS identity for NTFS ones. Avoid a UNIX identity on NTFS even where the SVM does have a CIFS server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Do not use &lt;code&gt;AVAILABLE&lt;/code&gt; as a connectivity check.&lt;/strong&gt; Make the acceptance criterion a PUT and a read-back after attaching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Budget tens of minutes into automation that creates a resource in ONTAP and then handles it through the AWS API.&lt;/strong&gt; Poll for the &lt;code&gt;fsvol-&lt;/code&gt; identifier rather than sleeping a fixed interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Write through the AWS management plane.&lt;/strong&gt; Where a setting can be made from either side, as with the junction path, the AWS side reflects faster (about 40 seconds against more than two minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Tear down through the AWS-side API.&lt;/strong&gt; A volume that has carried an access point can keep a NAS bucket on the ONTAP side, so a procedure built on ONTAP's &lt;code&gt;volume delete&lt;/code&gt; gets stuck there (&lt;code&gt;aws fsx delete-volume&lt;/code&gt; completes)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What the schedule took over
&lt;/h2&gt;

&lt;p&gt;Everything so far was about whether an operation is possible. Operations has another kind of work: checking capacity, taking stock of snapshots, revisiting tiering policy, confirming throughput is still right-sized. &lt;strong&gt;All of it is daily or weekly, and a button for it does not get pressed.&lt;/strong&gt; That work suits a schedule better than a screen, so six patterns live in &lt;code&gt;operations/&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;ID&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OPS1&lt;/td&gt;
&lt;td&gt;capacity-rightsizing&lt;/td&gt;
&lt;td&gt;Capacity and throughput monitoring, tier recommendation, what-if cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS2&lt;/td&gt;
&lt;td&gt;storage-efficiency&lt;/td&gt;
&lt;td&gt;Tracks deduplication and compression efficiency, recommends enabling them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS3&lt;/td&gt;
&lt;td&gt;tiering-optimizer&lt;/td&gt;
&lt;td&gt;Cold-data analysis, tiering policy recommendation and savings estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS4&lt;/td&gt;
&lt;td&gt;snapshot-lifecycle&lt;/td&gt;
&lt;td&gt;Retention policy compliance check and drift detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS5&lt;/td&gt;
&lt;td&gt;cost-optimization&lt;/td&gt;
&lt;td&gt;Cost breakdown, unit economics ($/GB), growth projection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS6&lt;/td&gt;
&lt;td&gt;qos-monitoring&lt;/td&gt;
&lt;td&gt;QoS policy adherence, bandwidth contention risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shape is the same in all six. An EventBridge schedule starts a Step Functions workflow, a VPC Lambda collects from the ONTAP REST API and CloudWatch, another analyses, and a third writes the report. Unit tests total 74 across the six (&lt;code&gt;capacity-rightsizing&lt;/code&gt; 27, &lt;code&gt;snapshot-lifecycle&lt;/code&gt; 19, &lt;code&gt;tiering-optimizer&lt;/code&gt; 11, &lt;code&gt;storage-efficiency&lt;/code&gt; 6, &lt;code&gt;cost-optimization&lt;/code&gt; 6, &lt;code&gt;qos-monitoring&lt;/code&gt; 5).&lt;/p&gt;

&lt;p&gt;The report has two destinations. With &lt;code&gt;OutputDestination=FSXN_S3AP&lt;/code&gt; it is written to an FSx for ONTAP volume through an S3 access point, which means &lt;strong&gt;anyone with the NFS or SMB mount reads it in the file explorer they already use.&lt;/strong&gt; On the real cluster the same 4,240-byte JSON appeared on the ONTAP volume as in the S3 bucket, the Step Functions execution took around 15 seconds, and four metrics were published to the &lt;code&gt;FSxOps&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;A recommendation comes out 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;"recommendation_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;"downsize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vol_backup_temp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0% (1024 GB allocated, 102 GB used)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommended_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Shrink to 205 GB or enable autosize (grow_shrink)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monthly_cost_delta_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-102.4&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;DemoMode=true&lt;/code&gt; runs the whole workflow on mock data with no file system, so the report layout and the shape of the recommendations can be reviewed first. Running cost itself — Lambda, Step Functions and S3 storage — came to roughly $1-5 a month for a daily run over a handful of file systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive automation — four levels by design, two in the code
&lt;/h3&gt;

&lt;p&gt;Honestly, here. The template's &lt;code&gt;AutomationLevel&lt;/code&gt; accepts 0 through 3.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Intended behaviour&lt;/th&gt;
&lt;th&gt;Implemented&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Report only&lt;/td&gt;
&lt;td&gt;Yes (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Notify over SNS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Execute after human review&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Auto-execute within guardrails&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The code branches in exactly one place, &lt;code&gt;automation_level &amp;gt;= 1&lt;/code&gt;, deciding whether to send SNS. &lt;strong&gt;So 2 or 3 deploys successfully and behaves identically to Level 1.&lt;/strong&gt; The values the parameter accepts and the values the code distinguishes do not match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The values listed in &lt;code&gt;AllowedValues&lt;/code&gt; are not a declaration of what is implemented — accepting a value and honouring it are different things.&lt;/strong&gt; It is the same point Part 4 made about not shipping buttons that cannot be pressed, showing up on the parameter side instead. Until the approval flow exists, narrowing &lt;code&gt;AllowedValues&lt;/code&gt; to 0 and 1 would be the accurate move.&lt;/p&gt;

&lt;p&gt;None of this replaces existing tooling. The vendor's CloudWatch dashboards and auto-resize solutions, and the SSD capacity auto-expansion AWS documents, differ in immediacy and in scope. The six here suit a flow where analysis and a recommendation come first and a person decides. Choose on the use case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On how recommendations are treated&lt;/strong&gt;: OPS4's snapshot deletion recommendations exclude anything below the minimum retention. A recommendation is an assistive signal, not a final decision. Which retention period satisfies your regulatory requirements is a legal and compliance judgement, and neither this article nor the pattern stands in for it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I did not measure
&lt;/h2&gt;

&lt;p&gt;"Unverified" is not "cannot be done". Every result above turned out that way, so on this path &lt;strong&gt;nothing is known until it is measured.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FlexClone at LUN granularity&lt;/td&gt;
&lt;td&gt;Unverified. Creating a LUN needs an iSCSI configuration, which is not on this path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The FlexClone split&lt;/td&gt;
&lt;td&gt;Unverified. Documentation only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An upper bound on the ONTAP-to-AWS reflection time&lt;/td&gt;
&lt;td&gt;Unverified. The three observations disagree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FabricPool tiering&lt;/td&gt;
&lt;td&gt;Unverified. Needs a different aggregate configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QoS / deduplication and compression / SnapMirror / Vscan&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SnapLock / Object Lock&lt;/td&gt;
&lt;td&gt;Unverified. &lt;strong&gt;Irreversible&lt;/strong&gt;, so not enabled without an instruction naming the retention value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attaching on a FlexCache cache volume&lt;/td&gt;
&lt;td&gt;Unverified. Requires a cluster peer and an SVM peer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS Levels 2 and 3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not implemented.&lt;/strong&gt; The approval flow and auto-execution are not in the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether the OPS recommendations are sound&lt;/td&gt;
&lt;td&gt;Not evaluated. They are produced; none was applied and measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPS across many file systems&lt;/td&gt;
&lt;td&gt;Unmeasured. Only a handful were run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;Two things were left off the screen, and both were written up here.&lt;/p&gt;

&lt;p&gt;On the ONTAP side I took NetApp's table as the starting point and measured on the S3 access point path. &lt;strong&gt;The four items it lists as unsupported did not appear as restrictions here.&lt;/strong&gt; Qtrees work, a quota refuses an S3 PUT, FlexClone works at both volume and file granularity, a FlexGroup takes an access point, and a clone of that FlexGroup can be created. The constraints were elsewhere: &lt;code&gt;AVAILABLE&lt;/code&gt; says nothing about the file system layer, something created in ONTAP takes tens of minutes to appear on the AWS side, and a volume that has carried an access point can only be deleted from the AWS side.&lt;/p&gt;

&lt;p&gt;On the operations side, the daily work went to a schedule rather than a screen. There is one mismatch between the declaration and the code here: &lt;strong&gt;&lt;code&gt;AutomationLevel&lt;/code&gt; accepts 0 through 3 while the code distinguishes only up to 1.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every failure in these six parts looked like a correct conclusion on its own. &lt;strong&gt;A conclusion is not settled until one more control, or a second reader, is added.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading all six parts. It started with showing files in a browser and ended outside the screen entirely. I hope the series is useful to anyone operating FSx for ONTAP, or building this kind of admin screen.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All test resources were deleted. The figures come from one specific environment and configuration and will differ with workload and setup. This article does not stand in for governance or regulatory judgement.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>fsxforontap</category>
      <category>ontap</category>
      <category>s3</category>
    </item>
    <item>
      <title>What I learned driving FSx for ONTAP from a file portal on AWS — FlexGroup creation, capacity rebalance, and deletes that refuse (Part 5)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:59:05 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-i-learned-driving-fsx-for-ontap-from-a-file-portal-on-aws-flexgroup-creation-capacity-3gkd</link>
      <guid>https://dev.to/aws-builders/what-i-learned-driving-fsx-for-ontap-from-a-file-portal-on-aws-flexgroup-creation-capacity-3gkd</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/putting-ontap-operations-on-a-file-portal"&gt;Part 4&lt;/a&gt; described putting 182 ONTAP operations on a file portal: the design of delegation and record, and the treatment of buttons that cannot be pressed and buttons that were never built.&lt;/p&gt;

&lt;p&gt;This part is what the running cluster refused while those 182 were being built. &lt;strong&gt;None of it was visible while reading the documentation; each one arrived as an error code.&lt;/strong&gt; That is the difference from last time: Part 4 was about design, this one is about measurement.&lt;/p&gt;

&lt;p&gt;The readers I have in mind are people writing code against the FSx for ONTAP ONTAP REST API. You do not need to be building a portal — if you call the same API, you stop in the same places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this part covers&lt;/strong&gt;: FlexGroup creation, FlexGroup capacity rebalance, how to read the capacity numbers, deletes that refuse, and four pitfalls in the S3-compatible API. Each with the response the system actually returned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this part does not cover&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ONTAP internals&lt;/li&gt;
&lt;li&gt;Absolute throughput figures (the subject here is whether something is possible and under what condition — but the defaults that decide whether a measured figure can be trusted are covered in a section below)&lt;/li&gt;
&lt;li&gt;Running SnapLock compliance (irreversible, and it would make a test file system undeletable for months, so it was deliberately not done)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Creating a FlexGroup through the ONTAP REST API requires naming the target aggregate.&lt;/strong&gt; FSx for ONTAP aggregates have tiering enabled, so ONTAP's automatic placement skips them. Creating one from the AWS console or the Amazon FSx API does not need this step&lt;/li&gt;
&lt;li&gt;The same root cause is &lt;strong&gt;documented on the FlexCache side as a flag (&lt;code&gt;use_tiered_aggregate&lt;/code&gt;), and it had not reached the volume-creation side&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The capacity rebalance &lt;code&gt;max_runtime&lt;/code&gt; has a lower and an upper bound that are not in the API reference.&lt;/strong&gt; With ONTAP's defaults it never starts&lt;/li&gt;
&lt;li&gt;Starting a rebalance &lt;strong&gt;enables an irreversible setting.&lt;/strong&gt; Stopping it does not undo it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;space.used&lt;/code&gt; means a different quantity depending on the volume.&lt;/strong&gt; Snapshots inside the reserve are not counted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deleted FlexClone child volume stays invisible to the API while still blocking the parent volume's delete.&lt;/strong&gt; There are two ways out, and both were measured&lt;/li&gt;
&lt;li&gt;Two places the documentation and the running system disagreed, and both changed the screen's wording&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six combinations of defaults return a plausible figure while measuring something other than what you intended.&lt;/strong&gt; Three of them come from reads being served out of cache&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dates&lt;/td&gt;
&lt;td&gt;mid-August 2026 to 2026-09-02 (JST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP version&lt;/td&gt;
&lt;td&gt;9.18.1P3D1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment type&lt;/td&gt;
&lt;td&gt;SINGLE_AZ_1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caller&lt;/td&gt;
&lt;td&gt;ONTAP REST API from a VPC Lambda (Python 3.13)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fsxadmin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup&lt;/td&gt;
&lt;td&gt;All test volumes deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The distinction that matters most — ONTAP's words and AWS's words
&lt;/h2&gt;

&lt;p&gt;Mix these up and searching for the error text will not find the answer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Where it appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;FabricPool&lt;/strong&gt;, &lt;code&gt;use_tiered_aggregate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ONTAP error text and REST field names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;tiering&lt;/strong&gt;, capacity pool tier&lt;/td&gt;
&lt;td&gt;AWS documentation and console&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They refer to the same mechanism. But &lt;strong&gt;the error arrives in ONTAP's vocabulary while the documentation explains it in AWS's&lt;/strong&gt;, so searching the AWS documentation for the error string returns nothing. The portal's wording follows the AWS side; only API strings are quoted as-is.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to watch when creating a FlexGroup
&lt;/h2&gt;

&lt;p&gt;A FlexGroup presents several "constituents" — internally split real volumes — as one namespace.&lt;/p&gt;

&lt;p&gt;To set the scope: FlexGroup volumes can be created from the AWS console, the AWS CLI and the Amazon FSx API (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/managing-volumes.html" rel="noopener noreferrer"&gt;Managing volumes&lt;/a&gt;). What follows is the &lt;strong&gt;ONTAP REST API&lt;/strong&gt; path, which is the one the portal uses.&lt;/p&gt;

&lt;p&gt;ONTAP normally picks the aggregates if you do not name them. On FSx for ONTAP that automatic selection does not resolve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aggregates not matching FabricPool requirements: aggr1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An FSx for ONTAP file system has a primary storage tier (SSD) and a capacity pool storage tier, and infrequently accessed data is tiered to the latter (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/volume-storage-capacity.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt;). Aggregates in that configuration fall outside ONTAP's automatic placement.&lt;/p&gt;

&lt;p&gt;The conditions came out one failure 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;Step&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create with default parameters&lt;/td&gt;
&lt;td&gt;Failed. &lt;code&gt;Volumes of this type must be at least 50GB&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry at 50 GiB&lt;/td&gt;
&lt;td&gt;Failed. &lt;code&gt;Aggregates not matching FabricPool requirements: aggr1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry naming the aggregate&lt;/td&gt;
&lt;td&gt;Failed. &lt;code&gt;Minimum size is "400GB"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 GiB, aggregate named, &lt;code&gt;tiering.policy=none&lt;/code&gt;, thin&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Naming the aggregate works. But &lt;strong&gt;that aggregate name appears in neither the AWS console nor the Amazon FSx API.&lt;/strong&gt; It has to be fetched with &lt;code&gt;GET /storage/aggregates&lt;/code&gt;. Nobody hands the tenant an aggregate name, so that extra call is mandatory.&lt;/p&gt;

&lt;p&gt;The 400 GB floor is configuration-dependent: the documented minimum is 100 GB per constituent, so the floor moves with the constituent count (the default is 8 per HA pair). And this extra step is only needed on the ONTAP REST API path — if you do not need to choose the aggregate yourself, creating the volume from the Amazon FSx API is shorter.&lt;/p&gt;

&lt;p&gt;The same root cause was already known on the FlexCache side. There is a flag, &lt;code&gt;use_tiered_aggregate&lt;/code&gt;, defaulting to &lt;code&gt;false&lt;/code&gt;. It had not reached the volume-creation side. &lt;strong&gt;The same cause surfacing elsewhere with a different face is a familiar shape in a managed service that wraps upstream software.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The screen carries this note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When creating a FlexGroup from this screen, the aggregate is named explicitly rather than placed automatically. FSx for ONTAP aggregates have tiering enabled and are excluded from automatic placement (measured: without it, creation fails with "Aggregates not matching FabricPool requirements"). With the default four constituents the minimum is 400 GB.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The capacity rebalance that never starts with the defaults
&lt;/h2&gt;

&lt;p&gt;A FlexGroup places files across constituents by hash, so they drift apart over time. &lt;strong&gt;When one fills, the whole volume returns "no space" even though the others have room.&lt;/strong&gt; A capacity rebalance is what corrects that.&lt;/p&gt;

&lt;p&gt;Running it against the real cluster produced two constraints on &lt;code&gt;max_runtime&lt;/code&gt; that are not in the API reference.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;th&gt;Error code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lower bound&lt;/td&gt;
&lt;td&gt;Under 30 minutes is refused&lt;/td&gt;
&lt;td&gt;&lt;code&gt;144182221&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upper bound&lt;/td&gt;
&lt;td&gt;Must be shorter than the time remaining until the next snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;13107433&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together, the window to start is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30 min &amp;lt;= max_runtime &amp;lt; (time from start until the next snapshot)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ONTAP's default &lt;code&gt;max_runtime&lt;/code&gt; is 6 hours. On a volume with the default snapshot policy (hourly at :05), 6 hours always violates the upper bound. &lt;strong&gt;With the value the portal offered as its default, it never started once.&lt;/strong&gt; With the default policy the only window is the 30 minutes from :05 to :35 each hour. On a frequent schedule such as &lt;code&gt;5min&lt;/code&gt;, nothing starts until the policy is detached.&lt;/p&gt;

&lt;p&gt;The boundary was pinned with a one-second A/B.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;&lt;code&gt;maxRuntime&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Until next snapshot&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16:06:12&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PT1H&lt;/code&gt; (60 min)&lt;/td&gt;
&lt;td&gt;58m47s&lt;/td&gt;
&lt;td&gt;Refused (&lt;code&gt;13107433&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:06:13&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PT30M&lt;/code&gt; (30 min)&lt;/td&gt;
&lt;td&gt;58m46s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;60 minutes &amp;gt; 58m47s refused; 30 minutes &amp;lt; 58m46s succeeded. &lt;strong&gt;Because the two calls are one second apart, the deciding factor is the &lt;code&gt;max_runtime&lt;/code&gt; value and not elapsed time.&lt;/strong&gt; ONTAP's own message names both remedies — "reduce the &lt;code&gt;-max-runtime&lt;/code&gt; or disable the snapshot policy" — because of this structure.&lt;/p&gt;

&lt;p&gt;The full observation log is in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/blob/main/solutions/amplify-portal/docs/flexgroup-rebalance-verification.md" rel="noopener noreferrer"&gt;the FlexGroup capacity rebalance record&lt;/a&gt;. State values absent from the reference's volume-side list turned up there too: &lt;code&gt;idle&lt;/code&gt; means running with nothing to move, &lt;code&gt;scheduled&lt;/code&gt; means a reservation exists. The portal had been displaying "running" as "unknown".&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%2Fvrl8lsv25wlms53mf3ud.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%2Fvrl8lsv25wlms53mf3ud.png" alt="The rebalance panel: state, volume-wide skew and the most skewed constituent, per-constituent usage, and the maximum runtime rounded to 30 minutes" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on irreversibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Starting a rebalance enables &lt;code&gt;granular data&lt;/code&gt; on that volume. It is required for the rebalance, and it creates two multi-part inodes per moved file (&lt;a href="https://docs.netapp.com/us-en/ontap/flexgroup/manage-flexgroup-rebalance-task.html" rel="noopener noreferrer"&gt;ONTAP documentation&lt;/a&gt;). &lt;strong&gt;Stopping does not undo it.&lt;/strong&gt; The vendor's documentation lists no way back other than deleting the volume or restoring from a snapshot taken before it was enabled.&lt;/p&gt;

&lt;p&gt;Stopping is not a rollback either: files already moved stay where they are. Both were measured (&lt;code&gt;granular_data&lt;/code&gt; remains &lt;code&gt;true&lt;/code&gt; after a stop). The screen's confirmation says both.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The space that does not come back after a delete
&lt;/h2&gt;

&lt;p&gt;Read the usage figure in a volume list naively and you will get it wrong, because &lt;code&gt;space.used&lt;/code&gt; means a different quantity depending on the volume.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapshots that fit inside the snapshot reserve (5% by default) are not counted in &lt;code&gt;space.used&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Anything past the reserve consumes the same space as live data and is counted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measurements. A 100 GiB volume reported &lt;code&gt;used&lt;/code&gt; of 18.1 MiB while holding 77.3 MiB of snapshots. That sits inside the 5% reserve (5 GiB), so the usage bar does not move. On a different volume with a 0% reserve, &lt;code&gt;used&lt;/code&gt; was 83,677 MiB: 81,934 MiB of live data and 1,743 MiB of snapshots. &lt;strong&gt;On 8 of 11 volumes, snapshots exceeded live data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the list was split from one usage figure into three: live data, snapshots, and reserve spill. Whether the space is not coming back because of snapshots or because live data fills it can now be read off the screen.&lt;/p&gt;

&lt;p&gt;Sometimes the cause is on another volume and its own row does not show it. That is FlexClone. A clone is based on one of the parent's snapshots, and that snapshot stays locked while the clone exists. Deleting files on the parent frees nothing while the locked snapshot still references the blocks. &lt;strong&gt;Reading the parent's row alone cannot tell you that&lt;/strong&gt;, so it is written on the FlexClone panel instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to check when a FlexClone volume will not delete
&lt;/h2&gt;

&lt;p&gt;After deleting a child volume created with FlexClone, deleting the parent volume is refused with &lt;code&gt;has one or more clones&lt;/code&gt;. I reproduced that on real hardware. The awkward part is that &lt;strong&gt;the API cannot see that child volume.&lt;/strong&gt; It is not in the volume listing, fetching it by name returns &lt;code&gt;entry doesn't exist&lt;/code&gt;, and yet only the parent's delete refuses, saying a clone exists.&lt;/p&gt;

&lt;p&gt;The cause is ONTAP's volume recovery queue, which retains deleted volumes for 12 hours by default. An A/B in the same environment, varying only whether the clone was split first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Parent delete&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delete an unsplit clone, then delete the parent&lt;/td&gt;
&lt;td&gt;Failed (still failing after 7 and 15 minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split the clone, delete it, then delete the parent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Succeeded within seconds&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exit was measured too. &lt;code&gt;GET /api/private/cli/volume/recovery-queue&lt;/code&gt; reads the queue. &lt;code&gt;POST .../purge&lt;/code&gt; goes through with &lt;code&gt;fsxadmin&lt;/code&gt; privilege, the entry left the queue in about 20 seconds, and the parent delete succeeded immediately afterwards.&lt;/p&gt;

&lt;p&gt;This is a place where &lt;strong&gt;I published something wrong and have corrected it.&lt;/strong&gt; The earlier text said purge needs diag privilege, is unreachable as &lt;code&gt;fsxadmin&lt;/code&gt;, and therefore the only option is to wait 12 hours. That was wrong. &lt;strong&gt;I blamed privilege without trying it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The screen's error carries the causation and both exits (split first, or purge). Purge cannot be undone, so it also says to confirm the volume was yours before running 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%2Fymiqhbd72zwp0y2wan6p.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%2Fymiqhbd72zwp0y2wan6p.png" alt="The FlexClone panel: the clone list and creation form, when to split, and the effect on the parent's capacity" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Four pitfalls in the S3-compatible API
&lt;/h2&gt;

&lt;p&gt;Behaviours worth knowing before you call an FSx for ONTAP S3 access point from a browser or an SDK. Knowing them changes how long the investigation takes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 501 failure of a "write only if absent" request
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PutObject&lt;/code&gt; with &lt;code&gt;if-none-match: *&lt;/code&gt; returns &lt;code&gt;501 NotImplemented&lt;/code&gt;, with the body &lt;code&gt;A header you provided implies functionality that is not implemented&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;Request&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PUT&lt;/code&gt; + &lt;code&gt;if-none-match: *&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;501 NotImplemented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PUT&lt;/code&gt; + &lt;code&gt;x-amz-checksum-crc32&lt;/code&gt; (no &lt;code&gt;if-none-match&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;200 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GET&lt;/code&gt; / &lt;code&gt;ListObjectsV2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;200 (neither header is sent)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CRC32 flexible checksums pass. Only &lt;code&gt;if-none-match&lt;/code&gt; fails. Reads are unaffected, so the symptom presents as &lt;strong&gt;"the listing works but every write fails."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not somebody else's problem. Amplify's Storage Browser translates its "do not overwrite" option into that header, so dropping it in unchanged breaks every upload and folder creation. If you need overwrite protection, look the key up before writing instead. That does not give you &lt;code&gt;if-none-match&lt;/code&gt;'s atomicity, and two concurrent writes to the same key can both decide it is absent.&lt;/p&gt;

&lt;h3&gt;
  
  
  GOVERNANCE and COMPLIANCE, indistinguishable from the error text
&lt;/h3&gt;

&lt;p&gt;S3 Object Lock has two retention modes. &lt;strong&gt;GOVERNANCE lets a user holding a specific permission (&lt;code&gt;s3:BypassGovernanceRetention&lt;/code&gt;) delete inside the retention period; COMPLIANCE lets nobody delete until the retention expires.&lt;/strong&gt; Deleting an object written with one day of retention, compared across both modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;GOVERNANCE&lt;/th&gt;
&lt;th&gt;COMPLIANCE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delete without bypass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AccessDenied ... object protected by object lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identical text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete with &lt;code&gt;BypassGovernanceRetention&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Succeeds&lt;/td&gt;
&lt;td&gt;Refused with identical text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because the text is identical, the response does not tell you why the bypass did not work. The first things to read are &lt;code&gt;get-object-lock-configuration&lt;/code&gt; and &lt;code&gt;head-object&lt;/code&gt;'s &lt;code&gt;ObjectLockMode&lt;/code&gt; rather than the IAM policy, because the same text covers both a missing permission and a mode that has no bypass.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lock indicator that does not change under automatic locking
&lt;/h3&gt;

&lt;p&gt;Assigning a policy whose schedules carry a &lt;code&gt;retentionPeriod&lt;/code&gt; leaves the locking feature flag at &lt;code&gt;false&lt;/code&gt;; only the assigned policy name changes. Reading the state alone suggests nothing will be locked, while every snapshot that policy takes is in fact locked. The judgement needs the assigned policy's &lt;code&gt;retentionPeriod&lt;/code&gt; as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  The usable endpoints, derived from the API rather than a config file
&lt;/h3&gt;

&lt;p&gt;An alias copied into a config file looks equally correct for an access point that has been deleted and for one that is &lt;code&gt;MISCONFIGURED&lt;/code&gt;. &lt;code&gt;fsx describe-s3-access-point-attachments&lt;/code&gt; returns &lt;code&gt;Lifecycle&lt;/code&gt; and the Internet or VPC origin, so derive the inventory from there. Always follow the pagination: read only the first page and &lt;strong&gt;an access point that exists goes missing in a way indistinguishable from one that does not.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two places the documentation and the running system disagreed
&lt;/h2&gt;

&lt;p&gt;Two things behaved differently from what I read before implementing them. Both changed the screen's wording.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subject&lt;/th&gt;
&lt;th&gt;Expected from the documentation&lt;/th&gt;
&lt;th&gt;Measured (ONTAP 9.18.1P3D1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deleting a QoS policy while assigned&lt;/td&gt;
&lt;td&gt;The CLI reference says it is refused without &lt;code&gt;-force&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;REST accepts it, and the volume's assignment is silently removed (every ceiling becomes 0, meaning unlimited)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforcement after deleting a quota rule&lt;/td&gt;
&lt;td&gt;The REST reference says it stays in effect until enforcement is cycled off and on&lt;/td&gt;
&lt;td&gt;The deleted rule's ceiling disappeared from the usage report immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The QoS confirmation text was rewritten. The correct warning is not "it cannot be deleted" but "&lt;strong&gt;deleting it removes the ceilings from every volume using that policy&lt;/strong&gt;". A note was added that releasing a single volume means assigning &lt;code&gt;none&lt;/code&gt;, not deleting the policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The six defaults that can invalidate a measurement
&lt;/h2&gt;

&lt;p&gt;Up to here the defaults decided whether an operation goes through. &lt;strong&gt;Measurement has defaults of the same shape.&lt;/strong&gt; None of them returns an error: each returns a plausible figure while measuring something other than what you intended.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where these figures come from&lt;/strong&gt;: a different measurement from the environment above. 2026-09-01 to 02, ap-northeast-1, ONTAP 9.18.1P3D1, SINGLE_AZ_1, throughput tiers 128 MBps and 2048 MBps, client c5n.9xlarge. Repeatability across repeat runs of the same measurement was 0.02–0.14%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sixth is from a different measurement.&lt;/strong&gt; 2026-09-06, ap-northeast-1, ONTAP 9.18.1P3D1,&lt;br&gt;
SINGLE_AZ_2, throughput 6,144 MBps, an SMB SVM joined to AWS Managed AD, clients c5n.2xlarge and&lt;br&gt;
c5n.9xlarge. A different generation and a different tier from the five above, so do not put the&lt;br&gt;
figures side by side.&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;Default&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;How to notice&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux NFS uses one TCP connection&lt;/td&gt;
&lt;td&gt;Stops around 590 MB/s and &lt;strong&gt;does not respond to stream count&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1 / 4 / 8 give the same figure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nconnect=16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dd if=/dev/zero&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero blocks skip disk and &lt;strong&gt;return at 4x the published ceiling&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Exceeds the published ceiling&lt;/td&gt;
&lt;td&gt;Use incompressible data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume inline efficiency is on&lt;/td&gt;
&lt;td&gt;Compressible or identical payloads collapse (4x on reads, under 5% on writes)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;space_savings.dedupe_percent&lt;/code&gt; is high&lt;/td&gt;
&lt;td&gt;Turn it off &lt;strong&gt;before writing&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DiskIopsConfiguration: AUTOMATIC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 IOPS/GiB, so IOPS becomes the limit&lt;/td&gt;
&lt;td&gt;MB/s divided by IOPS is implausibly small&lt;/td&gt;
&lt;td&gt;&lt;code&gt;USER_PROVISIONED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A read only slightly over the cache size&lt;/td&gt;
&lt;td&gt;The cache-served share dominates and &lt;strong&gt;the disk path is not measured&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DiskReadBytes&lt;/code&gt; divided by &lt;code&gt;DataReadBytes&lt;/code&gt; is a few per cent or less&lt;/td&gt;
&lt;td&gt;Read &lt;strong&gt;at least twice&lt;/strong&gt; the cache in one pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMB Multichannel ships disabled&lt;/td&gt;
&lt;td&gt;One channel, and it holds &lt;strong&gt;while the session reports &lt;code&gt;dialect=3.1.1&lt;/code&gt;&lt;/strong&gt; (1 MiB sequential read 942 against 1,824 MB/s)&lt;/td&gt;
&lt;td&gt;One established TCP connection to the SVM; &lt;code&gt;Get-SmbMultichannelConnection&lt;/code&gt; returns nothing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vserver cifs options modify -is-multichannel-enabled true&lt;/code&gt;. &lt;strong&gt;It does not reach existing connections — restart the client's SMB service and remap&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How the fifth one surfaced. I assumed 280 GiB would not fit in cache. Read through Amazon CloudWatch's &lt;code&gt;AWS/FSx&lt;/code&gt; namespace, &lt;code&gt;DiskReadBytes&lt;/code&gt; divided by &lt;code&gt;DataReadBytes&lt;/code&gt; came to 1.4 / 0.9 / 1.5%. &lt;strong&gt;98.5-99.9% of the bytes read never touched disk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 2048 MBps tier's in-memory cache is 256 GB, that is 238 GiB (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;FSx for ONTAP performance&lt;/a&gt;; ap-northeast-1 falls under the first-generation Single-AZ "all other Regions" table). &lt;strong&gt;280 GiB exceeds it by 18%, not by a factor of two or three. "Past the cache" and "not served from the cache" are different things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inline efficiency can be restored, provided you wait.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Rejected unless efficiency.op_state is idle
PATCH /api/storage/volumes/{uuid}  {"efficiency": {"compression": "inline"}}
PATCH /api/storage/volumes/{uuid}  {"efficiency": {"dedupe": "both"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;But only turn it off on a volume you can discard.&lt;/strong&gt; Immediately after writing, a background efficiency operation is running and the wait is not predictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting it in a runbook
&lt;/h2&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;What to write down&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;
&lt;strong&gt;To create a FlexGroup through the ONTAP REST API, fetch the aggregate with &lt;code&gt;GET /storage/aggregates&lt;/code&gt; and name it.&lt;/strong&gt; If you do not need to choose the aggregate, create the volume from the Amazon FSx API instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Set the rebalance &lt;code&gt;max_runtime&lt;/code&gt; to at least 30 minutes and below the time to the next snapshot.&lt;/strong&gt; Do not pass the default through&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Do not treat starting a rebalance as an operation you can press at any time.&lt;/strong&gt; &lt;code&gt;granular data&lt;/code&gt; is enabled irreversibly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Do not show usage as a single number.&lt;/strong&gt; Separate live data from snapshots, or nobody can reach the cause of "deleting files did not free space"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A teardown procedure for an environment with clones must include the split, or the recovery queue purge.&lt;/strong&gt; "The clone was deleted, so the parent can go" does not hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Errors arrive in ONTAP's vocabulary.&lt;/strong&gt; Keep the AWS-to-ONTAP term mapping at hand before searching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A runbook for measuring performance states how to drop the defaults first.&lt;/strong&gt; Leave any one of zero-filled data, inline efficiency, an unspecified &lt;code&gt;nconnect&lt;/code&gt; or &lt;code&gt;AUTOMATIC&lt;/code&gt; IOPS in place, and the figure produced is measuring something else&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I did not measure
&lt;/h2&gt;

&lt;p&gt;"Unverified" is not "impossible". As last time, the boundary stated as a boundary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The actual effect of a rebalance (how far the skew is evened out)&lt;/td&gt;
&lt;td&gt;Unmeasured. Only the conditions to start were measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance impact after &lt;code&gt;granular data&lt;/code&gt; is enabled&lt;/td&gt;
&lt;td&gt;Unmeasured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A recovery queue with a changed retention&lt;/td&gt;
&lt;td&gt;Unverified. Measured at the 12-hour default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual tiering into the capacity pool&lt;/td&gt;
&lt;td&gt;Unverified. Needs a different aggregate configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running SnapLock compliance&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Deliberately not done.&lt;/strong&gt; Irreversible, up to the file system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An alternative path for copies over 5 GiB (NFS or SMB)&lt;/td&gt;
&lt;td&gt;Unmeasured. The portal stops at refusing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Part 6 covers the other side: what was left off the screen. How much of ONTAP stays usable alongside the S3 access point path, and the operational work that was handed to a schedule instead of a button.&lt;/p&gt;




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

&lt;p&gt;Nothing in this part was visible while reading the documentation. Creating a FlexGroup through the ONTAP REST API needs the aggregate named, a capacity rebalance never starts with the defaults, &lt;code&gt;space.used&lt;/code&gt; means a different quantity per volume, and a deleted FlexClone volume blocks the parent's delete while invisible through the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they share is that the error text does not name the cause.&lt;/strong&gt; That is why the causation and the exits live on the screen. As with the one-second A/B on the rebalance, another explanation looked just as correct until one control was added.&lt;/p&gt;

&lt;p&gt;There is also one correction. I published "purge needs diag privilege, so wait 12 hours" and later found it goes through. &lt;strong&gt;I blamed privilege without trying it.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The figures come from one specific environment and configuration and will differ with workload and setup. The error codes were observed on ONTAP 9.18.1P3D1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>fsxforontap</category>
      <category>ontap</category>
      <category>storage</category>
    </item>
  </channel>
</rss>
