<?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>Has Apache Iceberg Really Solved Vendor Lock-in? Some Thoughts</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Sat, 26 Sep 2026 14:09:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/has-apache-iceberg-really-solved-vendor-lock-in-some-thoughts-527c</link>
      <guid>https://dev.to/aws-builders/has-apache-iceberg-really-solved-vendor-lock-in-some-thoughts-527c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/1f0c39d7332108" rel="noopener noreferrer"&gt;Icebergは本当にベンダーロックインを解消したのか考えてみた&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;With the spread of Apache Iceberg, I hear more and more that data platforms have become more open.&lt;br&gt;
"Iceberg is an open format, so you can avoid vendor lock-in" is an explanation I see often, and one I've given myself.&lt;/p&gt;

&lt;p&gt;Working with data platforms built on AWS, as well as Databricks, Snowflake, and Microsoft Fabric, I've found plenty of situations where that explanation holds true.&lt;br&gt;
But I started wondering how far it really goes, so this time I want to sort out which kinds of lock-in Iceberg has solved, and which it hasn't.&lt;/p&gt;

&lt;p&gt;To give the conclusion up front: what Iceberg achieved is &lt;strong&gt;data portability&lt;/strong&gt;.&lt;br&gt;
Governance, meaning permissions, auditing, lineage, and sharing, does not travel with the data in the same way.&lt;br&gt;
My current view is that lock-in hasn't disappeared; it has simply moved up to a higher layer.&lt;/p&gt;

&lt;p&gt;By the way, I'm a big fan of Unity Catalog!&lt;/p&gt;
&lt;h1&gt;
  
  
  Where Did Lock-in Used to Live?
&lt;/h1&gt;

&lt;p&gt;Let's start by looking back at how things used to be.&lt;/p&gt;

&lt;p&gt;In traditional data warehouses and data lakes, the file format, the way metadata was managed, transaction management, partition management, and the access APIs were all closed off inside the service.&lt;br&gt;
In that structure, "the data living inside that service" was itself the lock-in.&lt;br&gt;
Moving to a different environment meant starting by extracting all of the data and converting it.&lt;/p&gt;

&lt;p&gt;Open table formats are what solved this.&lt;br&gt;
Data files sit in cloud storage such as S3 as Parquet, and metadata sits right alongside them in an open specification.&lt;br&gt;
Because both specifications are public, neither is locked into a vendor-specific format.&lt;/p&gt;

&lt;p&gt;So what, then, is not open?&lt;/p&gt;
&lt;h1&gt;
  
  
  The Catalog Defined by the Iceberg Spec Is Actually Very Narrow
&lt;/h1&gt;

&lt;p&gt;Let's go back to the spec and check what role the catalog plays in Iceberg.&lt;/p&gt;

&lt;p&gt;The "Overview" section under "Catalog" in the official Apache Iceberg documentation explains that creating, dropping, and renaming tables are the catalog's responsibilities, and that its most important responsibility is tracking a table's current metadata and providing it when the table is loaded.&lt;br&gt;
It also says that multiple kinds of compute engines can share a common data layer by sharing the same catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://iceberg.apache.org/terms/" rel="noopener noreferrer"&gt;https://iceberg.apache.org/terms/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So how does that "current metadata" get updated?&lt;br&gt;
That's covered in the Table Spec.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://iceberg.apache.org/spec/" rel="noopener noreferrer"&gt;https://iceberg.apache.org/spec/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every change to table state creates a new metadata file, which replaces the old metadata through an atomic swap.&lt;br&gt;
Writers optimistically create a metadata file on the assumption that the current version won't change before they commit, and then commit by swapping the table's metadata pointer from the base version to the new version.&lt;br&gt;
This is what's known as compare-and-swap (CAS): even when multiple writers commit at the same time, updates based on stale state can be detected and treated as conflicts.&lt;/p&gt;

&lt;p&gt;Here's what that looks like as a diagram.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Catalog
┌──────────────────────────────────────┐
│  table_a → s3://.../v3.metadata.json │
└──────────────────────────────────────┘
              │
              │ Pointer to current metadata
              ▼
┌──────────────────────────┐
│  metadata.json           │  ← Open
│    schemas / snapshots   │
│    partition-specs       │
│  manifest list / files   │  ← Open
└──────────────────────────┘
              │
              ▼
┌──────────────────────────┐
│  Parquet / S3            │  ← Open
└──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schemas, snapshots, and partition specs all actually live inside &lt;code&gt;metadata.json&lt;/code&gt;.&lt;br&gt;
Managing the list of snapshots used for time travel is not the catalog's responsibility either.&lt;/p&gt;

&lt;p&gt;One thing worth pointing out here: the same Table Spec states that the atomic operation used to commit metadata depends on how tables are tracked, and that the spec does not standardize it.&lt;br&gt;
In other words, what the Table Spec defines stops at "swap the pointer atomically"; where and how that pointer is kept is left to the catalog implementation.&lt;br&gt;
That is the minimum set of catalog responsibilities visible from the table spec.&lt;/p&gt;

&lt;h1&gt;
  
  
  The REST Catalog Spec Broadened the Catalog's Role
&lt;/h1&gt;

&lt;p&gt;At the implementation level, though, things don't stop there.&lt;/p&gt;

&lt;p&gt;To support more languages and engines, the Iceberg project created a common API called the REST Catalog Protocol.&lt;br&gt;
And with the REST Catalog, the commit mechanism itself changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://iceberg.apache.org/rest-catalog-spec/" rel="noopener noreferrer"&gt;https://iceberg.apache.org/rest-catalog-spec/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;According to the "Commit protocol" section of the REST Catalog Protocol page, unlike client-side catalogs, a REST client neither writes metadata files nor swaps the pointer itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://iceberg.apache.org/docs/nightly/rest-protocol/" rel="noopener noreferrer"&gt;https://iceberg.apache.org/docs/nightly/rest-protocol/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead, the client sends the server the conditions that must still hold for the commit to be valid (Requirements), along with the metadata changes themselves (Updates).&lt;br&gt;
The server checks every requirement against the current metadata and, if they all hold, applies the updates and writes the new metadata.&lt;/p&gt;

&lt;p&gt;In other words, with the REST Catalog, responsibility for committing has moved from the client to the catalog.&lt;br&gt;
The same page explains that because the server owns the commit logic, it enables server-side conflict resolution, multi-table commits, and secure table sharing through credential vending or remote signing.&lt;/p&gt;

&lt;p&gt;Let's also look at credential vending.&lt;br&gt;
The server can include short-lived, table-scoped storage credentials (&lt;code&gt;storage-credentials&lt;/code&gt;) in its load-table response.&lt;br&gt;
However, whether to vend credentials is up to the server, and a separate mechanism called remote signing, where the server only signs requests, is also defined.&lt;br&gt;
So the accurate reading seems to be that the spec defines access delegation mechanisms as &lt;strong&gt;optional&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What the spec does not define, on the other hand, is the authorization model behind that delegation (how "who can access what" is expressed).&lt;br&gt;
Even though how you call credential vending is standardized, the logic that decides "who gets which permissions," and the way that is expressed, are left to each catalog implementation.&lt;/p&gt;

&lt;p&gt;This looks like the boundary with what real-world data catalogs actually cover.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Real-World Data Catalogs Cover
&lt;/h1&gt;

&lt;p&gt;When you look at catalogs as actual products, they take on even more.&lt;/p&gt;

&lt;p&gt;They offer a wide range of capabilities: namespace management, serving table metadata, credential vending, access control including row- and column-level security, tag-based and attribute-based permissions, data discovery, lineage, audit logs, data sharing, and integration with external tables and other catalogs.&lt;/p&gt;

&lt;p&gt;This is not at all a criticism of the products.&lt;br&gt;
What we actually expect from a catalog in production is, if anything, these additional capabilities.&lt;br&gt;
A pointer plus a credential vending endpoint doesn't settle the authorization logic of who can read which table, and it doesn't give you auditing either.&lt;/p&gt;

&lt;p&gt;The problem is that because these additional capabilities aren't open, each implementation ends up with its own unique shape.&lt;/p&gt;

&lt;p&gt;As a concrete example, let's look at Databricks Unity Catalog.&lt;br&gt;
Unity Catalog's credential vending is a mechanism that issues short-lived credentials to external systems connecting via the Unity REST API and the Iceberg REST Catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.databricks.com/aws/en/external-access/credential-vending" rel="noopener noreferrer"&gt;https://docs.databricks.com/aws/en/external-access/credential-vending&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "Credential Vending" section of Chapter 9, "Open Access," in the book &lt;em&gt;Data Governance with Unity Catalog on Databricks&lt;/em&gt; explains that the principal accessing the data must first be granted the necessary read or write permissions, and that each vending API call is recorded in the audit system table as &lt;code&gt;generateTemporaryTableCredential&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The credential vending entry point follows the REST Catalog spec, but both the permission model that decides "who gets which permissions" and the audit mechanism that records those calls are Unity Catalog's own implementation.&lt;br&gt;
Here, too, you can see access control and auditing converging in the same place: the catalog.&lt;/p&gt;

&lt;h1&gt;
  
  
  Catalog-Specific Capabilities Create Lock-in
&lt;/h1&gt;

&lt;p&gt;Organizing this structure from a migration perspective gives the following.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Standardization&lt;/th&gt;
&lt;th&gt;Moving to another catalog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data files&lt;/td&gt;
&lt;td&gt;Iceberg Table Spec&lt;/td&gt;
&lt;td&gt;◎ Usable as-is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;td&gt;Iceberg Table Spec&lt;/td&gt;
&lt;td&gt;◎ Usable as-is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How the metadata pointer is kept&lt;/td&gt;
&lt;td&gt;Not standardized (Table Spec only defines the atomic swap)&lt;/td&gt;
&lt;td&gt;○ Tables must be registered in the new catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog operation APIs, commits, how credential vending is called&lt;/td&gt;
&lt;td&gt;REST Catalog spec&lt;/td&gt;
&lt;td&gt;○ The calls are common, but whether credentials are vended and how permissions are decided depend on the implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control and permission model (who is allowed what)&lt;/td&gt;
&lt;td&gt;Out of spec&lt;/td&gt;
&lt;td&gt;× Reconfigure or rebuild per implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Views&lt;/td&gt;
&lt;td&gt;An Iceberg View Spec exists, but exposure and SQL dialects depend on the implementation&lt;/td&gt;
&lt;td&gt;△ to × Definitions may need migrating or rewriting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditing&lt;/td&gt;
&lt;td&gt;Usage metrics reporting is in the spec, but audit log format is not&lt;/td&gt;
&lt;td&gt;△ Integration or conversion needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lineage&lt;/td&gt;
&lt;td&gt;Outside the scope of the Iceberg spec&lt;/td&gt;
&lt;td&gt;△ to × Integration, conversion, or rebuilding needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data sharing&lt;/td&gt;
&lt;td&gt;Access delegation mechanisms are in the spec, but the sharing model is not&lt;/td&gt;
&lt;td&gt;△ Redesign depending on the sharing approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table maintenance&lt;/td&gt;
&lt;td&gt;Outside the scope of the Iceberg spec&lt;/td&gt;
&lt;td&gt;△ to × Operations need to be revisited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On auditing, the REST Catalog Protocol includes a mechanism where, if the server exposes a metrics endpoint, clients report scan and commit metrics to it.&lt;br&gt;
See the "Metrics reporting" section below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://iceberg.apache.org/docs/nightly/rest-protocol/" rel="noopener noreferrer"&gt;https://iceberg.apache.org/docs/nightly/rest-protocol/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the sense that catalog operators can see how tables are being used, you could say part of auditing is starting to make its way into the spec.&lt;br&gt;
However, it does not define the format of audit logs that record "who accessed what, and when" for governance purposes.&lt;/p&gt;

&lt;p&gt;On views, Unity Catalog's official documentation states that while the Iceberg REST Catalog API exposes tables, it does not expose views, so external Iceberg engines cannot read views defined in Unity Catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.databricks.com/aws/en/iceberg/" rel="noopener noreferrer"&gt;https://docs.databricks.com/aws/en/iceberg/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So there are cases where you can take your tables with you, but not the views you've built on top of them.&lt;/p&gt;

&lt;p&gt;The parts the Iceberg spec covers are genuinely open.&lt;br&gt;
The reason a catalog migration can be "just re-register the tables in the new catalog, without copying data" is precisely because the data and metadata layers are open.&lt;/p&gt;

&lt;p&gt;Of course, there are cases where you can export lineage information to another system or feed audit logs into an external platform.&lt;br&gt;
However, nothing in the Iceberg spec guarantees that you can move between catalogs while keeping the same semantics, the same permission structure, and the same operability.&lt;/p&gt;

&lt;p&gt;What gets locked in, I think, is not so much the catalog itself as the management and governance machinery built on the assumption of that catalog.&lt;br&gt;
If your permission settings, authorization model, auditing, lineage, sharing, and operational jobs depend on a specific catalog's features or representations, changing the catalog means revisiting all of them too.&lt;br&gt;
And much of what we expect from a catalog in production is concentrated right here.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Preconditions Behind "The Data Is Still in S3"
&lt;/h1&gt;

&lt;p&gt;So far, I've been writing on the premise that "the data remains in storage, and since it's Iceberg, other engines can read it."&lt;br&gt;
That premise, however, comes with conditions.&lt;/p&gt;

&lt;p&gt;The first is that the storage is under your own control.&lt;br&gt;
Unity Catalog's official documentation explicitly states that credential vending is not supported on workspaces that use default storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.databricks.com/aws/en/iceberg/" rel="noopener noreferrer"&gt;https://docs.databricks.com/aws/en/iceberg/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In setups where data lives in vendor-managed storage, the premise that "storage lock-in has been solved" may not hold in the first place.&lt;/p&gt;

&lt;p&gt;The second is that the table is actually managed as Iceberg.&lt;/p&gt;

&lt;p&gt;Unity Catalog's Iceberg REST Catalog provides read, write, and create access for managed Iceberg tables, but only read-only access for Delta Lake tables that have Iceberg reads enabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.databricks.com/aws/en/tables/managed" rel="noopener noreferrer"&gt;https://docs.databricks.com/aws/en/tables/managed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A table that is really Delta underneath and merely readable as Iceberg cannot be written to from outside, so the "◎ for data files and metadata" in the table above doesn't apply as-is.&lt;/p&gt;

&lt;p&gt;When evaluating data portability, it seems necessary to check not just the format, but also "where the data lives" and "which metadata is the source of truth."&lt;/p&gt;

&lt;h1&gt;
  
  
  Why Does Everything Converge on the Catalog?
&lt;/h1&gt;

&lt;p&gt;Why do capabilities like permissions, auditing, sharing, and lineage all end up concentrated in the catalog?&lt;/p&gt;

&lt;p&gt;This isn't speculation; the official Iceberg documentation itself explains it.&lt;br&gt;
The "Credential vending" section of the REST Catalog Protocol page states that because vended credentials are scoped to a table's storage prefixes, the catalog becomes the single point of access control.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://iceberg.apache.org/docs/nightly/rest-protocol/" rel="noopener noreferrer"&gt;https://iceberg.apache.org/docs/nightly/rest-protocol/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As mentioned earlier, with the REST Catalog the catalog also takes on commits.&lt;br&gt;
That means both the place that finalizes the table's state and the place that grants access to storage are the catalog.&lt;/p&gt;

&lt;p&gt;From here on this is my own interpretation, but if state management and access authorization both converge in one place, it's natural to record audits there too.&lt;br&gt;
The same goes for data sharing and lineage, and as a result, governance capabilities concentrate in the catalog.&lt;/p&gt;

&lt;p&gt;In other words, this concentration of capabilities in the catalog could be seen less as vendors trying to lock customers in, and more as a rational design that emerges from trying to centrally manage access to data.&lt;br&gt;
And I feel that this very rationality is what ends up creating a new kind of switching cost.&lt;/p&gt;

&lt;p&gt;On top of that, trying to standardize access control runs head-on into each cloud's IAM and identity platform.&lt;br&gt;
Permission settings from one environment don't carry over to another because the way principals are represented is fundamentally different, and reducing them to a common format isn't easy.&lt;br&gt;
I suspect this gap won't be closed anytime soon.&lt;/p&gt;

&lt;h1&gt;
  
  
  Does Using an OSS Catalog Avoid Lock-in?
&lt;/h1&gt;

&lt;p&gt;Let's also think about OSS here.&lt;/p&gt;

&lt;p&gt;Chapter 9, "Open Access," of &lt;em&gt;Data Governance with Unity Catalog on Databricks&lt;/em&gt;, the book I mentioned earlier, explains that because Unity Catalog supports both the Unity REST API and the Iceberg REST API, you can migrate to other catalogs that support these formats, which eases vendor lock-in concerns.&lt;br&gt;
The same chapter also introduces a setup that self-hosts the OSS version of Unity Catalog.&lt;/p&gt;

&lt;p&gt;I think this is correct when it comes to data portability.&lt;br&gt;
And if you keep using an OSS catalog, you may also be able to move to another cloud or another operator while keeping the same permission model.&lt;/p&gt;

&lt;p&gt;But what's solved in that case is "lock-in to a vendor," not "lock-in to a specific catalog implementation."&lt;br&gt;
When moving from one OSS catalog to another, or to a different managed catalog, you still need to revisit permissions, auditing, sharing, and operations.&lt;br&gt;
It may be closer to reality to say that the lock-in target has simply shifted from "the vendor" to "the catalog implementation."&lt;/p&gt;

&lt;h1&gt;
  
  
  Portability
&lt;/h1&gt;

&lt;p&gt;Based on all of this, I'd like to split the word "portability" into two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Portability
&lt;/h2&gt;

&lt;p&gt;Can the same data be read and written from a different environment, and can the data be migrated? This is what open table formats solved.&lt;br&gt;
That said, it assumes the storage is under your control and the tables are managed as Iceberg.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance Portability
&lt;/h2&gt;

&lt;p&gt;Can the same data be moved to another environment while keeping the same permission structure, the same auditing, the same sharing model, and the same operability?&lt;br&gt;
This is the part that remains unsolved.&lt;/p&gt;

&lt;p&gt;"Being able to take your data with you" and "being able to take the way your data is managed with you" are entirely different problems.&lt;br&gt;
Looking only at the former and declaring "lock-in has been solved" seems like evaluating only half the picture.&lt;/p&gt;

&lt;p&gt;I think it's closer to reality to say that lock-in hasn't disappeared, but that the layer where lock-in can occur has moved up.&lt;br&gt;
As storage, table format, catalog, and governance separate into distinct layers, the center of gravity has shifted upward by exactly as much as the lower layers have opened up.&lt;/p&gt;

&lt;h1&gt;
  
  
  Choosing a Catalog Is as Weighty a Decision as Choosing an Engine
&lt;/h1&gt;

&lt;p&gt;Engines also have their own SQL dialects, optimizations, UDFs, and operational features, and migrating between them is by no means easy.&lt;br&gt;
But I feel that changing catalogs, which means rebuilding permission design, audit design, and operations all at once, is a qualitatively different kind of cost from an engine migration.&lt;br&gt;
I think it's fair to treat choosing a catalog as a decision just as weighty as choosing an engine, and in some cases even more so.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;That said, it's also true that most of the time you'll use your data platform's standard catalog. Or rather, if you want to make full use of a data platform's features, you're often effectively required to use that product's catalog.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;This time, I sorted out the lock-in Iceberg has solved and the lock-in it hasn't, by separating things into layers.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the Table Spec opens up covers data files, metadata, and the concept of atomically swapping the pointer; how the pointer is kept is not standardized.&lt;/li&gt;
&lt;li&gt;The REST Catalog spec standardizes commits and access delegation such as credential vending as the catalog's role, and the official documentation itself positions the catalog as "the single point of access control."&lt;/li&gt;
&lt;li&gt;On the other hand, the permission model that decides "who gets which permissions," audit logs, lineage, and the sharing model are all outside the spec and specific to each catalog implementation.&lt;/li&gt;
&lt;li&gt;Data portability also has preconditions: the storage must be under your control, and the tables must be managed as Iceberg.&lt;/li&gt;
&lt;li&gt;Using an OSS catalog lets you avoid vendor lock-in, but lock-in to the catalog implementation remains.&lt;/li&gt;
&lt;li&gt;Lock-in hasn't disappeared; it seems closer to reality to say the layer where lock-in can occur has moved up, from storage and format to catalog and governance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think the term "open table format" does keep its promise when it comes to data and metadata.&lt;br&gt;
On the other hand, the catalog and governance layers above them are not guaranteed the same portability.&lt;/p&gt;

&lt;p&gt;"Being able to take your data out" and "being able to take your data's management and governance out" are separate problems.&lt;br&gt;
That's exactly why, when evaluating a data platform, I think we need to look not only at the openness of storage and table formats, but also at the portability of the catalog and governance layers above them.&lt;/p&gt;

&lt;p&gt;I hope this article is helpful to anyone thinking through catalog selection or governance design.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iceberg</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Building Scalable Context Layers for AI Agents with Elastic and AWS AgentCore</title>
      <dc:creator>N Chandra Prakash Reddy</dc:creator>
      <pubDate>Sat, 26 Sep 2026 10:25:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-scalable-context-layers-for-ai-agents-with-elastic-and-aws-agentcore-2g12</link>
      <guid>https://dev.to/aws-builders/building-scalable-context-layers-for-ai-agents-with-elastic-and-aws-agentcore-2g12</guid>
      <description>&lt;p&gt;I was at AWS Community Day Bengaluru recently on 11 July 2026 and the energy was simply fantastic. There were a lot of great presentations throughout the day, but one presentation really caught my eye. &lt;strong&gt;Someshwaran Mohan Kumar&lt;/strong&gt;, presenting on &lt;strong&gt;“Building Scalable Context Layers for AI Agents with Elastic Agent Builder and AWS AgentCore”&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let’s be honest, we’ve all had an experience with an AI chatbot that sounds very smart one minute and then can’t remember what we were talking about the next. Someshwaran tackled this same pain area. If you are designing AI apps, the principles covered in this webinar are absolute game changers!&lt;/p&gt;

&lt;p&gt;So here’s my thorough evaluation of the session, and how we can finally give our AI bots long-term memory.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Evolution of AI: We Are Beyond Simple Chatbots&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;To fix AI agents, we first need to understand how we got here. The discussion began with a very essential question: what exactly is a Large Language Model (LLM), and how is it different from an Agent? Someshwaran humorously describes our initial view of LLMs with the popular joke, merely typing away madly at a prompt.&lt;/p&gt;

&lt;p&gt;But the landscape has changed quickly during the past few years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;2022:&lt;/strong&gt; We began by building simple wrappers around chatbots with simple APIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early 2023:&lt;/strong&gt; We went to prompt chains, employing tools to link a series of tasks together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mid 2023:&lt;/strong&gt; We observed the emergence of tool-calling bots able to communicate with external functions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;2024:&lt;/strong&gt; Workflow engines added logic loops and human-in-the-loop judgments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;2025 to 2026:&lt;/strong&gt; We have officially reached the era of multi-agent systems in which specialized AI roles (e.g. Researchers, Writers, Reviewers) work over common states.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This evolution has been supported by extensive foundational research, on which the engineering community has depended. The emergence of Retrieval-Augmented Generation (RAG) for knowledge intensive tasks has been witnessed. We also learned about the “Lost in the Middle” phenomena, showing that LLMs suffer from a U-shaped performance curve – they recall the beginning and the end of a long prompt, but information in the center affects model performance dramatically. To alleviate these limitations, frameworks such as “ReAct” evolved, which combined reasoning traces with real actions in the environment.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The "Ghajini" Problem: Why Your AI Forgets You&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Now this is when it gets fun. Even with complex models, we run across a huge obstacle when designing agents. Agents are stateless by default.&lt;/p&gt;

&lt;p&gt;Someshwaran calls this the “Ghajini Problem” (or the Goldfish Problem). It's like dialing up your bank's customer service line. You talk for 10 minutes about your entire account history, your present problem, your preferences. The rep is helping you perfect yourself. But hang up and call back the next day and the new representative knows absolutely nothing. You have to start from scratch.&lt;/p&gt;

&lt;p&gt;And this happens with AI bots every single time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session Starts&lt;/strong&gt;: The user is shown the whole context, goals, preferences, and history.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Agent Responds:&lt;/strong&gt; The agent works well, uses the context and offers value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session Ends:&lt;/strong&gt; All is forgotten. The memory is wiped clean.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bottom line. Users have to repeat themselves a lot. The agents never learn new preferences, there is no continuity at all and personalization is not possible. It is a problem of context and a problem of agent.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Infrastructure Solution: AWS AgentCore&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;So how do we solve this? To build an effective agent, the two parts, the “Conversation Layer + Infra Layer” and the “Knowledge + Context Layer” must collaborate.&lt;/p&gt;

&lt;p&gt;The presentation walked over building the base infrastructure using AWS AgentCore. This is the strong wrapper you need for your custom bot. AWS AgentCore enables you to make use of essential runtime resources such as Gateway for safe API routing, Identity management, Policy enforcement for guardrails, and Evaluations for quality tracking.&lt;/p&gt;

&lt;p&gt;But the real magic is the fifth resource: &lt;strong&gt;Memory&lt;/strong&gt;. AWS AgentCore introduces session summarizers, analyst preferences and insights natively. It also allows observability to measure memory events API requests, latency and faults.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Code Walkthrough: Building with Strands SDK&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We then use the Strands SDK to actually construct this out. Someshwaran gave a good glimpse of how simple the entry point is for an agent developed on top of this architecture.&lt;/p&gt;

&lt;p&gt;This is what the code looks like to spin up a memory aware agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/main.py - Agent Entrypoint
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.runtime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockAgentCoreApp&lt;/span&gt;

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

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrock_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mcp_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;es_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ANALYST_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_manager&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;See the &lt;code&gt;@app.entrypoint&lt;/code&gt; wrapper? This means your method is a handler compatible with AgentCore, where the memory manager is automatically injected before and after each conversational round.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Context Solution: Elasticsearch&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;An agent needs to know the domain really well to be useful and AgentCore keeps track of the communication history. This is where Elasticsearch, the specific Context Layer, comes in.&lt;/p&gt;

&lt;p&gt;You might be asking yourself “Is Elasticsearch and AgentCore Memory doing the same thing?” They are not competing; they are complementing. AgentCore Memory is for discussion only (episodic memory, summaries, user preferences). Elasticsearch is, alternatively, domain-centric (RAG over live operational data, hybrid search and semantic insight storage).&lt;/p&gt;

&lt;p&gt;Together you obtain an agent that knows exactly what is happening in your business processes right now and remembers the user.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Bridging the Gap with MCP&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;But to be able to safely hook the agent into this huge pool of Elasticsearch data, we need a standardized means to communicate context. Enter MCP (Model Context Protocol).&lt;/p&gt;

&lt;p&gt;Someshwaran explains hilariously accurately with a real-world comparison why MCP exists – comparing how outsiders save phone contacts vs. how Indians save contacts. A foreigner might just save a number as “Alex” or “Mark”. Meanwhile, the Indian contact list reads “Tina Airtel”, “Pappu Jio”, “Crush Airtel.” We naturally provide metadata and context to everything to make it easily searchable. MCP achieves just that for AI models . It fills the gap to standardize passing of information and tools across .&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Before and After: Code Simplification&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The effect of MCP is huge for the developers. Let’s look at the difference Someshwaran presented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before MCP:&lt;/strong&gt; You had to build brittle, hard-coded integration code. To simply look for a server error we had to manually configure the client, hardcode the index and build sophisticated JSON match queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;elasticsearch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Elasticsearch&lt;/span&gt;

&lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Elasticsearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-elastic.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_logs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weblogs-*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error_code&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_logs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After MCP:&lt;/strong&gt; The code is beautifully clean. You start the agent, you define the bedrock model, and you just pass &lt;code&gt;mcp_client.tools&lt;/code&gt;. The Elastic MCP server offers tools (eg &lt;code&gt;execute_esql&lt;/code&gt; or &lt;code&gt;search_index&lt;/code&gt;) dynamically to the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bedrock_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mcp_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;  &lt;span class="c1"&gt;# discovered from Elastic MCP server
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Find the top 5 countries generating 5xx errors this week
and show the trend over time.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Instead of writing database queries you just ask the agent a natural language inquiry and it does the rest using the tools it dynamically discovered.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Architecture Deep Dive and Demo&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%2F4n1n5btsq8k0ksqpoesh.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%2F4n1n5btsq8k0ksqpoesh.jpeg" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture makes sense when it’s assembled. The Strands Agent orchestrator takes a user request and coordinates LLM processing with Amazon Bedrock, historical context with AgentCore Memory, and tool access with the MCP Client. The MCP Client connects to Elastic Cloud Serverless securely to query live indices.&lt;/p&gt;

&lt;p&gt;The live demo showed this architecture using an Elastic AI Agent UI. The user only asked “Describe what data I have available” and the agent took the autonomy to list the 8 indices present in the cluster, automatically categorizing them into CRM &amp;amp; Customer Data and Marketing Data.&lt;/p&gt;

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

&lt;p&gt;So here's the bottom line on building your own context-aware AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory turns a stranger into a trusted partner:&lt;/strong&gt; Think of going to your favorite neighborhood coffee shop every morning. When the waitress remembers you enjoy an oat milk latte with no sugar, you immediately feel loved. Without memory, your agent is like a stranger who forgets your face the second you walk out the door. AWS AgentCore gives your AI a friendly memory of user preferences and interaction history, spanning sessions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context works best as a team effort:&lt;/strong&gt; Don't ask one database to do everything. Think about ordering dinner on an app like Swiggy. AgentCore is like a saved profile, it remembers your dietary preferences and previous orders. Elasticsearch is the restaurant kitchen and delivery fleet – it’s keeping tabs on what ingredients are in stock, current order status and road conditions right now. Together, they ensure sure the user always gets the proper result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standardize how your tools connect:&lt;/strong&gt; Remember when every phone brand had a separate charging pin and you required a drawer full of adapters? It used to be as messy connecting tools to AI bots. The Model Context Protocol (MCP) is an universal USB-C cable. Your agent can plug directly into Elasticsearch tools, no hard-coded bespoke code needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If you are building a startup:&lt;/strong&gt; Now you can deploy customer care agents that remember earlier discussions, no more users needing to re-explain their concerns from scratch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For engineering and operations teams:&lt;/strong&gt; You can deploy internal AI assistants who automatically analyze live production logs, and troubleshoot faults, saving your team hours of manual querying.&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;By the conclusion of the day, this seminar changed how I think about modern AI architecture entirely. We’re finally out of the era of stateless, forgetful chatbots, and into the age where software can actually remember who we are, and grasp what’s occurring in our systems in real time.&lt;/p&gt;

&lt;p&gt;Constructing a memory-aware AI was once thought of as a hard problem, accessible only to specific research labs. But as Someshwaran has showed with AWS AgentCore and the Strands SDK, the infrastructure is already in place to handle the heavy lifting for us. Combining conversational memory with the instant search capabilities of Elasticsearch allows us to create AI applications that give true compounding value over time.&lt;/p&gt;

&lt;p&gt;If you want to roll up your sleeves and get your hands dirty with these architecture patterns, check out the resources over at &lt;a href="https://www.elastic.co/search-labs" rel="noopener noreferrer"&gt;elastic.co/search-labs&lt;/a&gt;. The tools are there – it’s our turn to construct!&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 Bengaluru&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speaker:&lt;/strong&gt; Someshwaran Mohan Kumar&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topic:&lt;/strong&gt; Building Scalable Context Layers for AI Agents with Elastic and AWS AgentCore&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date:&lt;/strong&gt; July 11, 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/3JrRXZWCJH8yutMgTzZRUmIczYm/building-scalable-context-layers-for-ai-agents-with-elastic-and-aws-agentcore" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopstour.hashnode.dev/building-scalable-context-layers-for-ai-agents-with-elastic-and-aws-agentcore" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Gemma 4 on Amazon SageMaker: QAT Weights Decode 2.05x Faster Than bf16 on One L4</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 25 Sep 2026 21:55:07 +0000</pubDate>
      <link>https://dev.to/aws-builders/gemma-4-on-amazon-sagemaker-qat-weights-decode-205x-faster-than-bf16-on-one-l4-318m</link>
      <guid>https://dev.to/aws-builders/gemma-4-on-amazon-sagemaker-qat-weights-decode-205x-faster-than-bf16-on-one-l4-318m</guid>
      <description>&lt;p&gt;This article gives a short background on Amazon SageMaker real-time endpoints, then measures Gemma 4 E2B's quantization-aware trained (QAT) checkpoint against the full-size bf16 release on the same NVIDIA L4 endpoint. A suite of Python MCP tools is built to simplify management of the vLLM hosted deployment.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Models&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; (bf16) and &lt;code&gt;google/gemma-4-E2B-it-qat-w4a16-ct&lt;/code&gt; (QAT, 4-bit weights)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;SageMaker &lt;code&gt;ml.g6.xlarge&lt;/code&gt;, 1x NVIDIA L4, 24 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;&lt;code&gt;us-east-2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software&lt;/td&gt;
&lt;td&gt;AWS vLLM SageMaker container, vLLM 0.30.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;QAT decodes at &lt;strong&gt;105.1 tok/s&lt;/strong&gt; against &lt;strong&gt;51.3&lt;/strong&gt;, and serves &lt;strong&gt;1077.25 tok/s&lt;/strong&gt; at 16 parallel requests against &lt;strong&gt;619.1&lt;/strong&gt;, with the same score on 40 checked questions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  SageMaker Hosting in Five Minutes
&lt;/h4&gt;

&lt;p&gt;SageMaker real-time inference is three objects, created in order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;A container image, its environment variables, and an IAM role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint config&lt;/td&gt;
&lt;td&gt;Which model runs on which instance types, and how many instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;The running HTTPS service, billed per instance-hour while it exists&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Requests go through &lt;code&gt;aws sagemaker-runtime invoke-endpoint&lt;/code&gt;, signed with your AWS credentials. SageMaker health-checks the container, routes traffic to it and writes its log to CloudWatch under &lt;code&gt;/aws/sagemaker/Endpoints/&amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The container here is the vLLM build AWS publishes for SageMaker. It reads vLLM settings from &lt;code&gt;SM_VLLM_&lt;/code&gt; environment variables, downloads the model from Hugging Face at start-up, and accepts OpenAI-style chat bodies. Switching checkpoints is one variable: &lt;code&gt;SM_VLLM_MODEL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;SageMaker JumpStart also lists Gemma 4 as ready-made packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sagemaker list-hub-contents &lt;span class="nt"&gt;--hub-name&lt;/span&gt; SageMakerPublicHub &lt;span class="nt"&gt;--hub-content-type&lt;/span&gt; Model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"HubContentSummaries[].HubContentName"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\t'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;gemma-4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;huggingface-llm-gemma-4-31b-it-nvfp4
huggingface-vlm-gemma-4-12b-it
huggingface-vlm-gemma-4-26b-a4b-it
huggingface-vlm-gemma-4-31b-it
huggingface-vlm-gemma-4-31b-it-fp8-block
huggingface-vlm-gemma-4-e2b-instruct
huggingface-vlm-gemma-4-e4b-it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the seven is a Google QAT checkpoint, so this comparison uses the vLLM container with a Hugging Face model ID.&lt;/p&gt;

&lt;p&gt;Three account limits shape every deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quota&lt;/strong&gt; is per instance type, per region, counted in instances. This account holds 1 for each single-L4 type in &lt;code&gt;us-east-1&lt;/code&gt;, &lt;code&gt;us-east-2&lt;/code&gt; and &lt;code&gt;us-west-2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity&lt;/strong&gt; is separate. &lt;code&gt;us-east-1&lt;/code&gt; and &lt;code&gt;us-west-2&lt;/code&gt; each left an L4 request in &lt;code&gt;Creating&lt;/code&gt; for about 30 minutes and then returned &lt;code&gt;InsufficientInstanceCapacity&lt;/code&gt;. &lt;code&gt;us-east-2&lt;/code&gt; placed one within minutes every time it was asked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fallback list&lt;/strong&gt; (&lt;code&gt;InstancePools&lt;/code&gt; in the endpoint config) holds quota for every type in it, so two endpoints cannot share one region's single-L4 quota.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With one L4 per region to work with, the two checkpoints ran one after the other on the same instance type in the same region.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where Do I Start?
&lt;/h4&gt;

&lt;p&gt;The deployment itself, from quota check to teardown with the aws CLI and the MCP server, is the first article in this pair: &lt;a href="https://dev.to/aws-builders/gemma-4-on-an-amazon-sagemaker-endpoint-aws-cli-nvidia-l4-and-an-mcp-server-2c9d"&gt;https://dev.to/aws-builders/gemma-4-on-an-amazon-sagemaker-endpoint-aws-cli-nvidia-l4-and-an-mcp-server-2c9d&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article starts from a working endpoint and changes one thing: the checkpoint.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;The repository above, with &lt;code&gt;aws login&lt;/code&gt; done and &lt;code&gt;mcp&lt;/code&gt; 2.x installed&lt;/li&gt;
&lt;li&gt;A SageMaker quota of at least 1 for &lt;code&gt;ml.g6.xlarge&lt;/code&gt; in a region with L4 capacity&lt;/li&gt;
&lt;li&gt;The full-size endpoint from the first article deployed as &lt;code&gt;gemma-4-e2b&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  What QAT Changes
&lt;/h4&gt;

&lt;p&gt;Google trains the QAT checkpoint with 4-bit weights in the loop, then exports it in the &lt;code&gt;compressed-tensors&lt;/code&gt; format vLLM reads natively. The &lt;code&gt;-w4a16-ct&lt;/code&gt; suffix means 4-bit weights, 16-bit activations. Google publishes the same model in four QAT forms; only this one loads in vLLM:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-qat-w4a16-ct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 loads, 4-bit weights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-qat-q4_0-unquantized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⚠️ stored at 16-bit, no memory saving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-qat-q4_0-gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ GGUF, for llama.cpp and Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-qat-mobile-*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ on-device formats&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Deploying it is the first article's Steps 4 to 6 with two variables changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma-4-e2b-qat
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MODEL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;google/gemma-4-E2B-it-qat-w4a16-ct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vLLM names the format when it starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quantization=compressed-tensors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  What the Engine Allocates
&lt;/h4&gt;

&lt;p&gt;The container log records the weights, the load time and the KV cache vLLM builds from what is left of the L4's memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model loading took 9.75 GiB memory and 82.751806 seconds
GPU KV cache size: 723,484 tokens, Maximum concurrency for 8,192 tokens per request: 88.32x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model loading took 8.01 GiB memory and 66.190265 seconds
GPU KV cache size: 867,999 tokens, Maximum concurrency for 8,192 tokens per request: 105.96x
&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;&lt;/th&gt;
&lt;th&gt;bf16&lt;/th&gt;
&lt;th&gt;QAT&lt;/th&gt;
&lt;th&gt;QAT / bf16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weights (GiB)&lt;/td&gt;
&lt;td&gt;9.75&lt;/td&gt;
&lt;td&gt;8.01&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache (tokens)&lt;/td&gt;
&lt;td&gt;723,484&lt;/td&gt;
&lt;td&gt;867,999&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weight load (s)&lt;/td&gt;
&lt;td&gt;82.75&lt;/td&gt;
&lt;td&gt;66.19&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create to &lt;code&gt;InService&lt;/code&gt; (min)&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;10.1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 4-bit weights save 18% of GPU memory. The checkpoint's own header shows why: only the transformer body is 4-bit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part of the QAT file&lt;/th&gt;
&lt;th&gt;GB&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;Stored as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-layer embedding&lt;/td&gt;
&lt;td&gt;4.698&lt;/td&gt;
&lt;td&gt;56.5%&lt;/td&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vocabulary embedding&lt;/td&gt;
&lt;td&gt;1.611&lt;/td&gt;
&lt;td&gt;19.4%&lt;/td&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformer body&lt;/td&gt;
&lt;td&gt;1.056&lt;/td&gt;
&lt;td&gt;12.7%&lt;/td&gt;
&lt;td&gt;packed 4-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio tower&lt;/td&gt;
&lt;td&gt;0.614&lt;/td&gt;
&lt;td&gt;7.4%&lt;/td&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision tower&lt;/td&gt;
&lt;td&gt;0.337&lt;/td&gt;
&lt;td&gt;4.1%&lt;/td&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The memory the smaller weights free goes to the KV cache.&lt;/p&gt;




&lt;h4&gt;
  
  
  How the Measurement Works
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;compare.py&lt;/code&gt; runs the same three measurements against each endpoint, at temperature 0, through the same &lt;code&gt;aws sagemaker-runtime invoke-endpoint&lt;/code&gt; call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decode.&lt;/strong&gt; Fixed-length replies of 16 and 512 tokens (&lt;code&gt;ignore_eos&lt;/code&gt;), five of each. The decode rate is (512 − 16) / (median time at 512 − median time at 16), which cancels the aws CLI start-up and the network round trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel requests.&lt;/strong&gt; 1, 4 and 16 requests at once, 256 tokens each, two batches per level. Throughput is total output tokens divided by the batch's wall time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answers.&lt;/strong&gt; 40 fixed questions with exact answers: 15 two-digit multiplications, 15 three-number sums, 10 capitals. Scored by regular expression.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 compare.py measure docs/runs/2026-09-25-qat-vs-bf16 gemma-4-e2b-qat@us-east-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wrote docs/runs/2026-09-25-qat-vs-bf16/measure-gemma-4-e2b-qat.json
{
  "decode_tokens_per_second": 105.1,
  "per_call_fixed_cost_seconds": 0.562,
  "load_tokens_per_second": {
    "1": 85.35,
    "4": 328.7,
    "16": 1077.25
  },
  "quality": "37/40"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;combine&lt;/code&gt; computes every ratio from the two result files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 compare.py combine compare.json measure-gemma-4-e2b.json measure-gemma-4-e2b-qat.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                               gemma-4-e2b     gemma-4-e2b-qat  ratio
weights_gib                           9.75                8.01  0.82
kv_cache_tokens                     723484              867999  1.2
load_seconds                     82.751806           66.190265  
decode_tokens_per_second              51.3               105.1  2.05
load_c1_tokens_per_second             45.6               85.35  1.87
load_c4_tokens_per_second            171.6               328.7  1.92
load_c16_tokens_per_second           619.1             1077.25  1.74
quality_correct                         37                  37  
identical answers: 35/40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Decode Speed
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;bf16&lt;/th&gt;
&lt;th&gt;QAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decode (tokens/s)&lt;/td&gt;
&lt;td&gt;51.3&lt;/td&gt;
&lt;td&gt;🥇 105.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512-token reply, fastest (s)&lt;/td&gt;
&lt;td&gt;10.491&lt;/td&gt;
&lt;td&gt;🥇 5.392&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512-token reply, slowest (s)&lt;/td&gt;
&lt;td&gt;10.661&lt;/td&gt;
&lt;td&gt;🥇 5.526&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-call client cost (s)&lt;/td&gt;
&lt;td&gt;0.633&lt;/td&gt;
&lt;td&gt;0.562&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;QAT decodes 2.05x faster.&lt;/strong&gt; The slowest QAT reply finished in about half the time of the fastest bf16 one, so the gap is far larger than the spread between repeats.&lt;/p&gt;

&lt;p&gt;Each decode step reads every transformer layer's weights from GPU memory, so decode speed follows how many bytes those layers take. Those are the layers the QAT export stores at 4 bits. The embeddings are looked up one row per token, so their 16-bit size costs memory and little time.&lt;/p&gt;




&lt;h4&gt;
  
  
  Parallel Requests
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests at once&lt;/th&gt;
&lt;th&gt;bf16 tok/s&lt;/th&gt;
&lt;th&gt;QAT tok/s&lt;/th&gt;
&lt;th&gt;QAT / bf16&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;45.6&lt;/td&gt;
&lt;td&gt;🥇 85.35&lt;/td&gt;
&lt;td&gt;1.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;171.6&lt;/td&gt;
&lt;td&gt;🥇 328.7&lt;/td&gt;
&lt;td&gt;1.92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;619.1&lt;/td&gt;
&lt;td&gt;🥇 1077.25&lt;/td&gt;
&lt;td&gt;1.74&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QAT leads at every level. The ratio narrows at 16, where a batch shares each weight read across more requests and the per-token saving counts for less. The single-request figures sit below the decode rate because each call also pays the aws CLI start-up.&lt;/p&gt;




&lt;h4&gt;
  
  
  Does It Still Answer Correctly?
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Questions&lt;/th&gt;
&lt;th&gt;bf16&lt;/th&gt;
&lt;th&gt;QAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiplication (15)&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a + b − c (15)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capitals (10)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total (40)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;35 of the 40 answers are identical character for character. The other five are all sums, and each model misses three of them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;bf16&lt;/th&gt;
&lt;th&gt;QAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;876 + 608 − 558&lt;/td&gt;
&lt;td&gt;926&lt;/td&gt;
&lt;td&gt;❌ 1026&lt;/td&gt;
&lt;td&gt;✅ 926&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;257 + 388 − 290&lt;/td&gt;
&lt;td&gt;355&lt;/td&gt;
&lt;td&gt;✅ 355&lt;/td&gt;
&lt;td&gt;❌ 655&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;782 + 571 − 534&lt;/td&gt;
&lt;td&gt;819&lt;/td&gt;
&lt;td&gt;❌ 829&lt;/td&gt;
&lt;td&gt;✅ 819&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;470 + 238 − 983&lt;/td&gt;
&lt;td&gt;−275&lt;/td&gt;
&lt;td&gt;✅ −275&lt;/td&gt;
&lt;td&gt;❌ −285&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150 + 937 − 139&lt;/td&gt;
&lt;td&gt;948&lt;/td&gt;
&lt;td&gt;❌ 918&lt;/td&gt;
&lt;td&gt;❌ 1010&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two models make the same number of mistakes on different questions. Forty questions are enough to show a large loss and too few to measure a small one.&lt;/p&gt;




&lt;h4&gt;
  
  
  Re-Measured in A-B-A Order
&lt;/h4&gt;

&lt;p&gt;Each endpoint runs on its own physical instance, and the two ran 15 minutes apart. To check that the gap belongs to the checkpoint, the bf16 endpoint was deployed a second time after QAT and measured again:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Start (UTC)&lt;/th&gt;
&lt;th&gt;Decode tok/s&lt;/th&gt;
&lt;th&gt;16 at once tok/s&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;18:36&lt;/td&gt;
&lt;td&gt;51.3&lt;/td&gt;
&lt;td&gt;619.1&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QAT&lt;/td&gt;
&lt;td&gt;18:50&lt;/td&gt;
&lt;td&gt;105.1&lt;/td&gt;
&lt;td&gt;1077.25&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bf16 again&lt;/td&gt;
&lt;td&gt;19:10&lt;/td&gt;
&lt;td&gt;51.5&lt;/td&gt;
&lt;td&gt;625.15&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two bf16 runs agree within 1.21% on every speed figure and give identical answers to all 40 questions. The second bf16 instance loaded its weights in 82.09 s against 82.75 s the first time.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare to Other Deployments
&lt;/h4&gt;

&lt;p&gt;The same bf16 model on the same GPU has been measured on two other platforms in this series, with &lt;code&gt;vllm bench serve&lt;/code&gt; at 128 output tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;1 request tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run, NVIDIA L4&lt;/td&gt;
&lt;td&gt;49.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 &lt;code&gt;g6.2xlarge&lt;/code&gt;, NVIDIA L4&lt;/td&gt;
&lt;td&gt;46.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SageMaker &lt;code&gt;ml.g6.xlarge&lt;/code&gt;, NVIDIA L4&lt;/td&gt;
&lt;td&gt;45.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three sit within 9% of each other. SageMaker's figure includes the aws CLI start-up in every call; its decode rate with that removed is 51.3.&lt;/p&gt;

&lt;p&gt;On a Tesla T4, QAT decoded 1.79x faster than bf16 for the same model. On the L4 the ratio is 2.05x.&lt;/p&gt;

&lt;p&gt;The methods differ: the other runs used &lt;code&gt;vllm bench serve&lt;/code&gt; with random prompts, other vLLM versions and their own hosts. Read the rows as a shape.&lt;/p&gt;




&lt;h4&gt;
  
  
  And Price/Performance?
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;ml.g6.xlarge&lt;/code&gt; in &lt;code&gt;us-east-2&lt;/code&gt; is $1.1267 an hour on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws pricing get-products &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--service-code&lt;/span&gt; AmazonSageMaker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERM_MATCH,Field&lt;span class="o"&gt;=&lt;/span&gt;instanceName,Value&lt;span class="o"&gt;=&lt;/span&gt;ml.g6.xlarge &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERM_MATCH,Field&lt;span class="o"&gt;=&lt;/span&gt;regionCode,Value&lt;span class="o"&gt;=&lt;/span&gt;us-east-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ml.g6.xlarge USE2-Host:ml.g6.xlarge 1.1267000000 Hrs | $1.1267 per Hosting ml.g6.xlarge hour in US East (Ohio)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same hourly price buys twice the tokens. Per million output tokens (arithmetic):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests at once&lt;/th&gt;
&lt;th&gt;bf16 $/M&lt;/th&gt;
&lt;th&gt;QAT $/M&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;6.86&lt;/td&gt;
&lt;td&gt;🥇 3.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1.82&lt;/td&gt;
&lt;td&gt;🥇 0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;td&gt;🥇 0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The endpoint bills while it exists, busy or idle, so these figures hold only while it is kept busy.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;bf16&lt;/th&gt;
&lt;th&gt;QAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decode speed&lt;/td&gt;
&lt;td&gt;51.3 tok/s&lt;/td&gt;
&lt;td&gt;🥇 105.1 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 requests at once&lt;/td&gt;
&lt;td&gt;619.1 tok/s&lt;/td&gt;
&lt;td&gt;🥇 1077.25 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory for weights&lt;/td&gt;
&lt;td&gt;9.75 GiB&lt;/td&gt;
&lt;td&gt;🥇 8.01 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checked answers&lt;/td&gt;
&lt;td&gt;37 / 40&lt;/td&gt;
&lt;td&gt;37 / 40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per million tokens at 16&lt;/td&gt;
&lt;td&gt;$0.51&lt;/td&gt;
&lt;td&gt;🥇 $0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On an L4 SageMaker endpoint, the QAT checkpoint is the default choice for Gemma 4 E2B: the same instance, one changed environment variable, about twice the tokens per dollar, and no measured change in answers. Keep bf16 as the reference when a task's accuracy needs a larger evaluation than 40 questions.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Stops the Meter
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;delete_endpoint&lt;/code&gt; removes the endpoint, its config and its model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🗑️ `gemma-4-e2b-qat` in `us-east-2`
- endpoint: deleted
- endpoint-config: deleted
- model: deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Creating&lt;/code&gt; endpoint refuses deletion, so a deploy runs until it reaches &lt;code&gt;InService&lt;/code&gt; or &lt;code&gt;Failed&lt;/code&gt; before it can be removed.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to measure what Gemma 4 E2B's QAT checkpoint changes on a SageMaker L4 endpoint. The key to the solution was changing only &lt;code&gt;SM_VLLM_MODEL&lt;/code&gt; between two deployments on the same instance type, and measuring decode speed with the per-call client cost removed. The measured results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 QAT decodes at &lt;strong&gt;105.1 tok/s&lt;/strong&gt; against bf16's &lt;strong&gt;51.3&lt;/strong&gt;, 2.05x&lt;/li&gt;
&lt;li&gt;🟢 At 16 requests at once QAT serves &lt;strong&gt;1077.25 tok/s&lt;/strong&gt; against &lt;strong&gt;619.1&lt;/strong&gt;, 1.74x&lt;/li&gt;
&lt;li&gt;🟢 Both score &lt;strong&gt;37 of 40&lt;/strong&gt; on checked questions, with 35 identical answers&lt;/li&gt;
&lt;li&gt;🟢 A second bf16 deployment after QAT reproduced the first within 1.21%&lt;/li&gt;
&lt;li&gt;⚠️ The 4-bit export saves 18% of weight memory, because the embeddings stay at 16-bit&lt;/li&gt;
&lt;li&gt;⚠️ Getting an L4 took three regions: &lt;code&gt;us-east-1&lt;/code&gt; and &lt;code&gt;us-west-2&lt;/code&gt; returned &lt;code&gt;InsufficientInstanceCapacity&lt;/code&gt; after about 30 minutes each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one account, SageMaker &lt;code&gt;ml.g6.xlarge&lt;/code&gt; with one NVIDIA L4 in &lt;code&gt;us-east-2&lt;/code&gt;, vLLM 0.30.0 from the AWS container, three deployments on 2026-09-25 each on its own instance, measured in bf16, QAT, bf16 order. Prompts were short; long-prompt behaviour was not measured. Decode used five replies per length, parallel throughput two batches per level, and quality 40 questions at temperature 0. Every request went through the aws CLI from one client machine.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for SageMaker deployment and benchmarking was validated with an incremental step by step approach.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/xbill9/sagemaker-gemma" rel="noopener noreferrer"&gt;https://github.com/xbill9/sagemaker-gemma&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part one, deploying Gemma 4 to SageMaker: &lt;a href="https://dev.to/aws-builders/gemma-4-on-an-amazon-sagemaker-endpoint-aws-cli-nvidia-l4-and-an-mcp-server-2c9d"&gt;https://dev.to/aws-builders/gemma-4-on-an-amazon-sagemaker-endpoint-aws-cli-nvidia-l4-and-an-mcp-server-2c9d&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gemma 4 E2B QAT w4a16: &lt;a href="https://huggingface.co/google/gemma-4-E2B-it-qat-w4a16-ct" rel="noopener noreferrer"&gt;https://huggingface.co/google/gemma-4-E2B-it-qat-w4a16-ct&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gemma 4 E2B: &lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;https://huggingface.co/google/gemma-4-E2B-it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gemma 4 on a Tesla T4, QAT vs bf16: &lt;a href="https://dev.to/gde/gemma-4-on-a-tesla-t4-qat-weights-decode-179x-faster-than-bf16-2fi4"&gt;https://dev.to/gde/gemma-4-on-a-tesla-t4-qat-weights-decode-179x-faster-than-bf16-2fi4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;2B Gemma 4 on Cloud Run with an NVIDIA L4: &lt;a href="https://dev.to/gde/2b-gemma-4-deployment-with-cloud-run-nvidia-l4-mcp-sdk-2x-and-claude-code-4ml3"&gt;https://dev.to/gde/2b-gemma-4-deployment-with-cloud-run-nvidia-l4-mcp-sdk-2x-and-claude-code-4ml3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SageMaker real-time inference: &lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/realtime-endpoints.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/sagemaker/latest/dg/realtime-endpoints.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS Deep Learning Containers: &lt;a href="https://github.com/aws/deep-learning-containers" rel="noopener noreferrer"&gt;https://github.com/aws/deep-learning-containers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;compressed-tensors: &lt;a href="https://github.com/neuralmagic/compressed-tensors" rel="noopener noreferrer"&gt;https://github.com/neuralmagic/compressed-tensors&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>sagemaker</category>
      <category>gemma</category>
      <category>vllm</category>
    </item>
    <item>
      <title>Gemma 4 on an Amazon SageMaker Endpoint: AWS CLI, NVIDIA L4, and an MCP Server</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 25 Sep 2026 21:50:14 +0000</pubDate>
      <link>https://dev.to/aws-builders/gemma-4-on-an-amazon-sagemaker-endpoint-aws-cli-nvidia-l4-and-an-mcp-server-2c9d</link>
      <guid>https://dev.to/aws-builders/gemma-4-on-an-amazon-sagemaker-endpoint-aws-cli-nvidia-l4-and-an-mcp-server-2c9d</guid>
      <description>&lt;p&gt;This article provides a step by step deployment guide for Gemma 4 E2B to an Amazon SageMaker hosted GPU enabled system. A suite of Python MCP tools is built to simplify management of the vLLM hosted deployment with Claude Code.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  What is this project trying to Do?
&lt;/h4&gt;

&lt;p&gt;This project serves Gemma 4 E2B from a SageMaker real-time endpoint on one NVIDIA L4 GPU, using the vLLM container AWS publishes for SageMaker. Every AWS call is a plain &lt;code&gt;aws&lt;/code&gt; CLI command, so each step can be run by hand or by the MCP server.&lt;/p&gt;

&lt;p&gt;A SageMaker real-time endpoint is a managed HTTPS inference server. SageMaker places the container on a GPU instance, health-checks it, routes requests to it and writes its logs to CloudWatch. There is no instance to patch, no security group to open and no load balancer to build.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where do I start?
&lt;/h4&gt;

&lt;p&gt;The strategy for starting MCP development for model management is a incremental step by step approach.&lt;/p&gt;

&lt;p&gt;First, the basic development environment is setup with the required system variables and a working Claude Code configuration.&lt;/p&gt;

&lt;p&gt;Then, the Python MCP server is brought up over stdio and validated with Claude Code in the local environment. The deployment follows as eight steps, each shown as the raw &lt;code&gt;aws&lt;/code&gt; command and the MCP tool that runs it.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;An AWS account and the AWS CLI v2, signed in with &lt;code&gt;aws login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A SageMaker endpoint quota of at least 1 for a single-L4 instance type (&lt;code&gt;ml.g6.xlarge&lt;/code&gt;, &lt;code&gt;ml.g6.2xlarge&lt;/code&gt; or &lt;code&gt;ml.g6.4xlarge&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Python 3.11 or newer with &lt;code&gt;mcp&lt;/code&gt; 2.x&lt;/li&gt;
&lt;li&gt;Claude Code or Gemini CLI installed and working&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jq&lt;/code&gt; for reading JSON replies&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Setup the Basic Environment
&lt;/h4&gt;

&lt;p&gt;Clone the repository and install the one requirement into the system Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xbill9/sagemaker-gemma
&lt;span class="nb"&gt;cd &lt;/span&gt;sagemaker-gemma
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; is gitignored. It holds the settings every tool reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Copy to .env (gitignored) and edit. sm.py reads it, so the MCP server, CLI and Makefile all see it.
AWS_REGION=us-east-2
MODEL_ID=google/gemma-4-E2B-it
INSTANCE_TYPE=ml.g6.xlarge
ENDPOINT_NAME=gemma-4-e2b
ROLE_NAME=sagemaker-gemma-execution-role
MAX_MODEL_LEN=8192
# Leave empty to use the newest SageMaker vLLM image in the region.
IMAGE_URI=
# Fallback instance types, highest priority first (same GPU keeps runs comparable).
INSTANCE_POOLS=ml.g6.xlarge,ml.g6.2xlarge,ml.g6.4xlarge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4 is Apache-2.0 and ungated on Hugging Face, so no Hugging Face token is needed.&lt;/p&gt;




&lt;h4&gt;
  
  
  Model Management Tool with MCP Stdio Transport
&lt;/h4&gt;

&lt;p&gt;The simplest MCP transport is stdio: the client launches the server as a local process and talks to it over stdin and stdout. In this project Claude Code is the MCP client. The server is one file, &lt;code&gt;server.py&lt;/code&gt;, on the MCP Python SDK 2.x:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RIG_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;READ_ONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readOnlyHint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotentHint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WRITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destructiveHint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DESTRUCTIVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destructiveHint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool carries one of the three annotations, so a client can tell a status check from a deploy from a delete.&lt;/p&gt;

&lt;p&gt;The tools do no AWS work themselves. They call &lt;code&gt;sm.py&lt;/code&gt;, which runs each request as an &lt;code&gt;aws&lt;/code&gt; CLI subprocess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run `aws &amp;lt;args&amp;gt; --output json` and return the parsed result.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI renews an &lt;code&gt;aws login&lt;/code&gt; session on its own, so a server that stays up for hours keeps working credentials. &lt;code&gt;sm.py&lt;/code&gt; also drops any &lt;code&gt;AWS_SESSION_TOKEN&lt;/code&gt; the server inherits from its parent process, because a static token expires inside a long-running server and outranks the login session.&lt;/p&gt;




&lt;h4&gt;
  
  
  Running the Python Code
&lt;/h4&gt;

&lt;p&gt;The project can be linted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All checks passed!
16 files already formatted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and tested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;----------------------------------------------------------------------
Ran 16 tests in 0.010s

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

&lt;/div&gt;



&lt;p&gt;The tests replace the &lt;code&gt;aws&lt;/code&gt; subprocess with a fake, so they run offline with no credentials. One of them compares the registered tool set and annotations against a fixed list: a tool that failed to register, or a delete that lost its destructive flag, fails the suite. ✅&lt;/p&gt;




&lt;h4&gt;
  
  
  Test the Protocol by Hand
&lt;/h4&gt;

&lt;p&gt;A client speaks JSON-RPC over stdio. Hold stdin open with &lt;code&gt;sleep&lt;/code&gt;, or the server sees end-of-input and exits before it answers:&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="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"notifications/initialized"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 server.py 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 sagemaker-gemma
2 ['check_quotas', 'delete_endpoint', 'deploy_endpoint', 'find_vllm_image', 'get_deployment_config', 'get_endpoint_logs', 'get_endpoint_status', 'get_help', 'list_endpoints', 'query_model', 'verify_model_health']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🟢 The server answers the handshake and lists 11 tools.&lt;/p&gt;




&lt;h4&gt;
  
  
  Claude Code .mcp.json
&lt;/h4&gt;

&lt;p&gt;Claude Code reads &lt;code&gt;.mcp.json&lt;/code&gt; in the project directory and launches the server with the system &lt;code&gt;python3&lt;/code&gt;:&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;"sagemaker-gemma"&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;"python3"&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;"/home/xbill/sagemaker-gemma/server.py"&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;Gemini CLI reads the same entry from &lt;code&gt;.gemini/settings.json&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Validation with Claude Code
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp get sagemaker-gemma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sagemaker-gemma:
  Scope: Project config (shared via .mcp.json)
  Status: ✔ Connected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From inside Claude Code, &lt;code&gt;get_help&lt;/code&gt; returns the resolved settings and the order of work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### sagemaker-gemma&lt;/span&gt;

Serving &lt;span class="sb"&gt;`google/gemma-4-E2B-it`&lt;/span&gt; with vLLM on a &lt;span class="gs"&gt;**SageMaker real-time endpoint**&lt;/span&gt;, managed
entirely through the aws CLI.

| Setting | Value |
| --- | --- |
| Region | &lt;span class="sb"&gt;`us-east-2`&lt;/span&gt; |
| Endpoint | &lt;span class="sb"&gt;`gemma-4-e2b`&lt;/span&gt; |
| Instance types (priority order) | &lt;span class="sb"&gt;`ml.g6.xlarge`&lt;/span&gt;, &lt;span class="sb"&gt;`ml.g6.2xlarge`&lt;/span&gt;, &lt;span class="sb"&gt;`ml.g6.4xlarge`&lt;/span&gt; |
| Max model length | &lt;span class="sb"&gt;`8192`&lt;/span&gt; |
| Image | &lt;span class="sb"&gt;`newest SageMaker vLLM image (find_vllm_image)`&lt;/span&gt; |
| Execution role | &lt;span class="sb"&gt;`sagemaker-gemma-execution-role`&lt;/span&gt; |

&lt;span class="gs"&gt;**Order of work:**&lt;/span&gt; check_quotas → deploy_endpoint → get_endpoint_status until
&lt;span class="sb"&gt;`InService`&lt;/span&gt; (about 10 minutes once an instance is placed) → verify_model_health
→ query_model → delete_endpoint.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The steps below follow that order. Each one shows the raw CLI command; &lt;code&gt;deploy_endpoint&lt;/code&gt; runs Steps 2 to 6 in one call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-2 &lt;span class="nv"&gt;AWS_PAGER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma-4-e2b
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MODEL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;google/gemma-4-E2B-it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 1 — Check the Endpoint Quota
&lt;/h4&gt;

&lt;p&gt;SageMaker endpoint quotas are per instance type, per region, and count instances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws service-quotas list-service-quotas &lt;span class="nt"&gt;--service-code&lt;/span&gt; sagemaker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Quotas[?QuotaName=='ml.g6.xlarge for endpoint usage'].Value"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
    1.0
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;check_quotas&lt;/code&gt; tool reads the same quota for each fallback type across the US regions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Instance | us-east-1 | us-east-2 | us-west-1 | us-west-2 |
| --- | ---: | ---: | ---: | ---: |
| `ml.g6.xlarge` | 1 | 1 | - | 1 |
| `ml.g6.2xlarge` | 1 | 1 | - | 1 |
| `ml.g6.4xlarge` | 1 | 1 | - | 1 |

Regions with quota for at least one of these types: us-east-1, us-east-2, us-west-2.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;-&lt;/code&gt; means the type is not offered in that region. A &lt;code&gt;0&lt;/code&gt; means a quota increase request first.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: One Quota Call per Region
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;list-service-quotas&lt;/code&gt; pages through every SageMaker quota in the region, and the Service Quotas API is rate limited per account. Twelve calls at once, one per type per region, came back as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An error occurred (TooManyRequestsException) when calling the ListServiceQuotas operation (reached max retries: 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check_quotas&lt;/code&gt; makes one call per region, one region at a time, and filters the types from that one result. &lt;code&gt;sm.py&lt;/code&gt; also sets &lt;code&gt;AWS_RETRY_MODE=adaptive&lt;/code&gt; so the CLI backs off and retries.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Find the vLLM Container
&lt;/h4&gt;

&lt;p&gt;The AWS vLLM repository holds pinned release tags such as &lt;code&gt;0.30.0-gpu-py312-cu130-ubuntu24.04-sagemaker-v1.1&lt;/code&gt;, floating aliases such as &lt;code&gt;0.30-gpu-py312&lt;/code&gt;, and &lt;code&gt;-soci&lt;/code&gt; index tags. Older release lines receive patch rebuilds, so the most recent push can carry an older vLLM. Sort the pinned tags by version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecr describe-images &lt;span class="nt"&gt;--registry-id&lt;/span&gt; 763104351884 &lt;span class="nt"&gt;--repository-name&lt;/span&gt; vllm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"imageDetails[].imageTags[]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\t'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'^[0-9]+\.[0-9]+\.[0-9]+-.*-sagemaker-v[0-9]+\.[0-9]+$'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;763104351884.dkr.ecr.&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;.amazonaws.com/vllm:&lt;span class="nv"&gt;$TAG&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;763104351884.dkr.ecr.us-east-2.amazonaws.com/vllm:0.30.0-gpu-py312-cu130-ubuntu24.04-sagemaker-v1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;find_vllm_image&lt;/code&gt; tool applies the same rule. The image runs vLLM 0.30.0.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Create the Execution Role
&lt;/h4&gt;

&lt;p&gt;SageMaker assumes this role to pull the image and write logs. It is created once per account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; sagemaker-gemma-execution-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; &lt;span class="s1"&gt;'{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sagemaker.amazonaws.com"},"Action":"sts:AssumeRole"}]}'&lt;/span&gt;
aws iam attach-role-policy &lt;span class="nt"&gt;--role-name&lt;/span&gt; sagemaker-gemma-execution-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/AmazonSageMakerFullAccess
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ROLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws iam get-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; sagemaker-gemma-execution-role &lt;span class="nt"&gt;--query&lt;/span&gt; Role.Arn &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a second run &lt;code&gt;create-role&lt;/code&gt; reports &lt;code&gt;EntityAlreadyExists&lt;/code&gt;; &lt;code&gt;get-role&lt;/code&gt; still sets &lt;code&gt;ROLE&lt;/code&gt;. &lt;code&gt;deploy_endpoint&lt;/code&gt; creates the role only when it is missing.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Create the Model
&lt;/h4&gt;

&lt;p&gt;A SageMaker model pairs an image with its settings. The container turns each &lt;code&gt;SM_VLLM_&lt;/code&gt; variable into the matching vLLM flag: &lt;code&gt;SM_VLLM_MAX_MODEL_LEN&lt;/code&gt; becomes &lt;code&gt;--max-model-len&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sagemaker create-model &lt;span class="nt"&gt;--model-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt; &lt;span class="nt"&gt;--execution-role-arn&lt;/span&gt; &lt;span class="nv"&gt;$ROLE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--primary-container&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Image&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Environment&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SM_VLLM_MODEL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL_ID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SM_VLLM_MAX_MODEL_LEN&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;8192&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SM_VLLM_GPU_MEMORY_UTILIZATION&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0.9&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight 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;"ModelArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sagemaker:us-east-2:&amp;lt;account-id&amp;gt;:model/gemma-4-e2b"&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;h4&gt;
  
  
  Step 5 — Create the Endpoint Config With Fallback Instance Types
&lt;/h4&gt;

&lt;p&gt;The endpoint config says where the model runs. &lt;code&gt;InstancePools&lt;/code&gt; lists up to five instance types in priority order, and SageMaker places the first one with a free instance. All three types below carry one L4, so the model sees the same GPU whichever is placed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sagemaker create-endpoint-config &lt;span class="nt"&gt;--endpoint-config-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--production-variants&lt;/span&gt; &lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;VariantName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;AllTraffic&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ModelName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;InitialInstanceCount&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1,
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;InstancePools&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;InstanceType&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ml.g6.xlarge&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Priority&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1},
                       {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;InstanceType&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ml.g6.2xlarge&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Priority&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:2},
                       {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;InstanceType&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ml.g6.4xlarge&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Priority&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:3}],
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ContainerStartupHealthCheckTimeoutInSeconds&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1800,
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ModelDataDownloadTimeoutInSeconds&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1800}]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"EndpointConfigArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sagemaker:us-east-2:&amp;lt;account-id&amp;gt;:endpoint-config/gemma-4-e2b"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two 1800-second timeouts give the container time to download the weights and compile before SageMaker's health check gives up.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: A Fallback List Holds Quota for Every Type in It
&lt;/h4&gt;

&lt;p&gt;With one Gemma endpoint running on &lt;code&gt;ml.g6.xlarge&lt;/code&gt; from the three-type list above, a second endpoint asking for &lt;code&gt;ml.g6.2xlarge&lt;/code&gt; in the same region was refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ResourceLimitExceeded: The account-level service limit 'ml.g6.2xlarge for endpoint usage' is 1 Instances, with current utilization of 1 Instances and a request delta of 1 Instances.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a quota of 1 per type, a second endpoint goes in another region, or uses types outside the first endpoint's list.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Create the Endpoint
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sagemaker create-endpoint &lt;span class="nt"&gt;--endpoint-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt; &lt;span class="nt"&gt;--endpoint-config-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"EndpointArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sagemaker:us-east-2:&amp;lt;account-id&amp;gt;:endpoint/gemma-4-e2b"&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;Billing starts when an instance is placed. &lt;code&gt;aws sagemaker wait endpoint-in-service --endpoint-name $NAME&lt;/code&gt; blocks until it is ready; &lt;code&gt;get_endpoint_status&lt;/code&gt; reports the same state and the instance type that was placed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ `gemma-4-e2b` in `us-east-2`: **InService**
- Instance: `ml.g6.xlarge`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container log is in CloudWatch, and &lt;code&gt;get_endpoint_logs&lt;/code&gt; tails it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws logs &lt;span class="nb"&gt;tail&lt;/span&gt; /aws/sagemaker/Endpoints/&lt;span class="nv"&gt;$NAME&lt;/span&gt; &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start-up on the L4, in minutes after &lt;code&gt;create-endpoint&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;Phase&lt;/th&gt;
&lt;th&gt;Minutes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weights loaded, 9.75 GiB in 82.75 s&lt;/td&gt;
&lt;td&gt;7.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache sized, 723,484 tokens&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  🔎 Tip: A Creating Endpoint Cannot Be Deleted
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;create-endpoint&lt;/code&gt; cannot be undone until the endpoint settles. A &lt;code&gt;delete-endpoint&lt;/code&gt; sent while it is &lt;code&gt;Creating&lt;/code&gt; is refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws: [ERROR]: An error occurred (ValidationException) when calling the DeleteEndpoint operation: Cannot update in-progress endpoint "arn:aws:sagemaker:us-east-2:&amp;lt;account-id&amp;gt;:endpoint/gemma-4-e2b".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint finishes starting, bills from the moment its instance is placed, and can be deleted once it reaches &lt;code&gt;InService&lt;/code&gt; or &lt;code&gt;Failed&lt;/code&gt;. Check the model ID and instance types before Step 6.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Capacity Is Separate From Quota
&lt;/h4&gt;

&lt;p&gt;A quota of 1 lets you request one instance; the region still has to have one free. In &lt;code&gt;us-east-1&lt;/code&gt;, two requests for L4 instances each stayed in &lt;code&gt;Creating&lt;/code&gt; for about 30 minutes and then failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to provision requested ML compute capacity due to InsufficientInstanceCapacity error. Please retry using a different ML instance type or after some time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same request in &lt;code&gt;us-east-2&lt;/code&gt; placed an instance at once. While SageMaker waits for capacity, no container starts and the CloudWatch log group never appears, which is how &lt;code&gt;get_endpoint_logs&lt;/code&gt; tells a capacity wait from a slow model load. No charge accrues during the wait. When it fails, repeat Steps 4 to 6 in another region where Step 1 shows the same quota.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Cross Check the Deployed Model
&lt;/h4&gt;

&lt;p&gt;The vLLM container accepts an OpenAI chat body on &lt;code&gt;invoke-endpoint&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"messages":[{"role":"user","content":"Why is the sky blue?"}],"max_tokens":256}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; req.json
aws sagemaker-runtime invoke-endpoint &lt;span class="nt"&gt;--endpoint-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--content-type&lt;/span&gt; application/json &lt;span class="nt"&gt;--body&lt;/span&gt; fileb://req.json out.json
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.choices[0].message.content'&lt;/span&gt; out.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'{model,usage}'&lt;/span&gt; out.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "ContentType": "application/json",
    "InvokedProductionVariant": "AllTraffic"
}
The sky is blue due to a phenomenon called **Rayleigh scattering**. This process is caused by how sunlight interacts with the Earth's atmosphere.
{"model":"google/gemma-4-E2B-it","usage":{"prompt_tokens":15,"total_tokens":271,"completion_tokens":256,"prompt_tokens_details":null,"completion_tokens_details":null}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verify_model_health&lt;/code&gt; sends one short request and checks for a reply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ model=`google/gemma-4-E2B-it` tokens=2 wall=0.749s reply='ok'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 8 — Teardown
&lt;/h4&gt;

&lt;p&gt;The endpoint bills by the hour until it is deleted. Deleting the config and the model as well leaves nothing behind. &lt;code&gt;delete_endpoint&lt;/code&gt; runs all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sagemaker delete-endpoint &lt;span class="nt"&gt;--endpoint-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt;
aws sagemaker delete-endpoint-config &lt;span class="nt"&gt;--endpoint-config-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt;
aws sagemaker delete-model &lt;span class="nt"&gt;--model-name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each command prints nothing and exits 0. &lt;code&gt;list_endpoints&lt;/code&gt; confirms the account is clear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### 0 endpoint(s) matching `gemma`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The goal of this article was to deploy Gemma 4 E2B to an Amazon SageMaker real-time endpoint with the AWS CLI and manage it from an MCP server. The key to the solution was the AWS vLLM SageMaker container, which turns the deployment into three &lt;code&gt;create-&lt;/code&gt; calls and makes the endpoint answer OpenAI-style chat requests. The deployment results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Eight CLI steps take an account from quota check to a serving endpoint and back to nothing&lt;/li&gt;
&lt;li&gt;🟢 The endpoint reached &lt;code&gt;InService&lt;/code&gt; 9.9 minutes after &lt;code&gt;create-endpoint&lt;/code&gt;, with the weights using 9.75 GiB of the L4's 24 GB&lt;/li&gt;
&lt;li&gt;🟢 The MCP server runs every step from Claude Code, marks each tool read-only, write or destructive, and passes its tests offline&lt;/li&gt;
&lt;li&gt;🟢 &lt;code&gt;InstancePools&lt;/code&gt; gives one endpoint config several fallback instance types&lt;/li&gt;
&lt;li&gt;⚠️ L4 capacity varied by region: &lt;code&gt;us-east-1&lt;/code&gt; refused twice, about 30 minutes each, while &lt;code&gt;us-east-2&lt;/code&gt; placed an instance at once&lt;/li&gt;
&lt;li&gt;⚠️ A fallback list holds quota for every type in it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one account, &lt;code&gt;ml.g6.xlarge&lt;/code&gt; with one NVIDIA L4, vLLM 0.30.0 from the AWS container, &lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; at full precision, deployed in &lt;code&gt;us-east-2&lt;/code&gt; on 2026-09-25. Start-up times are from a single deployment.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for SageMaker deployment was validated with an incremental step by step approach.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/xbill9/sagemaker-gemma" rel="noopener noreferrer"&gt;https://github.com/xbill9/sagemaker-gemma&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gemma 4 E2B on Hugging Face: &lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;https://huggingface.co/google/gemma-4-E2B-it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS Deep Learning Containers: &lt;a href="https://github.com/aws/deep-learning-containers" rel="noopener noreferrer"&gt;https://github.com/aws/deep-learning-containers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SageMaker real-time inference: &lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/realtime-endpoints.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/sagemaker/latest/dg/realtime-endpoints.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CreateEndpointConfig API: &lt;a href="https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateEndpointConfig.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateEndpointConfig.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP Python SDK: &lt;a href="https://github.com/modelcontextprotocol/python-sdk" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/python-sdk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM: &lt;a href="https://docs.vllm.ai/" rel="noopener noreferrer"&gt;https://docs.vllm.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>sagemaker</category>
      <category>gemma</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Two Iceberg Clients, One Protocol: Where the Time Goes</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 25 Sep 2026 16:40:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/two-iceberg-clients-one-protocol-where-the-time-goes-4a2c</link>
      <guid>https://dev.to/aws-builders/two-iceberg-clients-one-protocol-where-the-time-goes-4a2c</guid>
      <description>&lt;p&gt;This article provides a step by step comparison of the Rust and Python clients for Apache Iceberg REST catalogs. It times both clients on the same operations against the same tables, then breaks one request down to see where the time goes.&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;h4&gt;
  
  
  What is this project trying to Do?
&lt;/h4&gt;

&lt;p&gt;Two Apache clients talk to the same catalogs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;iceberg-catalog-rest&lt;/code&gt; 0.10.1&lt;/strong&gt;, the Rust client, built in release mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pyiceberg&lt;/code&gt; 0.12.0&lt;/strong&gt;, the Python client, on Python 3.14.7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They run against three catalogs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apache Polaris&lt;/strong&gt; 1.7.0 in Docker on the same machine, so there is no network time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google BigLake&lt;/strong&gt; and &lt;strong&gt;Microsoft OneLake&lt;/strong&gt; over the internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three things are measured: how long each call takes, how long each client takes to start, and where the time in one call is spent.&lt;/p&gt;

&lt;p&gt;The benchmark measures speed only. It does not check that answers are correct, and a client that lacks an operation you need is the wrong choice however fast it is.&lt;/p&gt;




&lt;h4&gt;
  
  
  Why Not Just Count Features?
&lt;/h4&gt;

&lt;p&gt;The Rust client supports 13 of the 25 catalog endpoints tested, and &lt;code&gt;pyiceberg&lt;/code&gt; supports 21. Anyone can count that from the two repositories, and it only holds for these two versions.&lt;/p&gt;

&lt;p&gt;How long a call takes is in neither repository, so that is what this article measures.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where do I start?
&lt;/h4&gt;

&lt;p&gt;The strategy for comparing the two clients is an incremental step by step approach.&lt;/p&gt;

&lt;p&gt;First, both clients are timed against a local catalog, where only the client's own time is measured. Then one request is broken down to find where the time goes. The same benchmark then runs against two managed catalogs, the two clients' requests are compared, and startup time is measured last.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Rust 1.94 or newer, and a &lt;strong&gt;release&lt;/strong&gt; build — this run used &lt;code&gt;rustc&lt;/code&gt; 1.98.1&lt;/li&gt;
&lt;li&gt;Python 3.10+ with &lt;code&gt;pyiceberg&lt;/code&gt; 0.12.0&lt;/li&gt;
&lt;li&gt;Docker, for the local Polaris catalog&lt;/li&gt;
&lt;li&gt;A quiet machine — this one is a 16-core Linux host&lt;/li&gt;
&lt;li&gt;Optional: a managed Iceberg REST catalog, for the internet runs&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Build Both Clients and Start Polaris
&lt;/h4&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;git clone https://github.com/xbill9/lakehouse-iceberg-2026
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;lakehouse-iceberg-2026/iceberg-conformance &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./polaris-up.sh
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;POLARIS_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root &lt;span class="nv"&gt;POLARIS_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;s3cr3t
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../iceberg-rust-client &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benchmark will not save results from a debug build.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Time Both Clients Locally
&lt;/h4&gt;

&lt;p&gt;To keep the comparison fair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each client runs in its own new process.&lt;/strong&gt; The benchmark script is Python, so running &lt;code&gt;pyiceberg&lt;/code&gt; inside it would give Python a head start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup is timed separately.&lt;/strong&gt; Each process connects and warms up before any call is timed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clients take turns.&lt;/strong&gt; They alternate going first, so a machine that slows down slows both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every timing is saved&lt;/strong&gt;, and the medians are computed from the saved files.
&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;python3 bench.py &lt;span class="nt"&gt;--rounds&lt;/span&gt; 4 &lt;span class="nt"&gt;--iters&lt;/span&gt; 15
&lt;span class="go"&gt;apache-polaris     6 ops, 2 clients, 4 rounds x 15 iters = 720 samples
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 bench_report.py
&lt;span class="go"&gt;wrote evidence/bench-comparison.txt   8 run(s) across 3 catalog(s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six read operations both clients support, median time in microseconds, lowest to highest across four runs on two days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;op&lt;/th&gt;
&lt;th&gt;rust&lt;/th&gt;
&lt;th&gt;pyiceberg&lt;/th&gt;
&lt;th&gt;python ÷ rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;list_namespaces&lt;/td&gt;
&lt;td&gt;301.3–768.1&lt;/td&gt;
&lt;td&gt;883.2–1479.4&lt;/td&gt;
&lt;td&gt;1.90x–4.29x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;load_namespace&lt;/td&gt;
&lt;td&gt;312.2–699.3&lt;/td&gt;
&lt;td&gt;784.7–1451.2&lt;/td&gt;
&lt;td&gt;2.08x–3.52x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;head_namespace&lt;/td&gt;
&lt;td&gt;328.4–584.8&lt;/td&gt;
&lt;td&gt;1048.6–1362.7&lt;/td&gt;
&lt;td&gt;2.33x–3.19x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list_tables&lt;/td&gt;
&lt;td&gt;327.0–552.1&lt;/td&gt;
&lt;td&gt;1003.2–1551.3&lt;/td&gt;
&lt;td&gt;2.56x–3.53x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;load_table&lt;/td&gt;
&lt;td&gt;4634.1–5764.4&lt;/td&gt;
&lt;td&gt;6405.6–7344.8&lt;/td&gt;
&lt;td&gt;1.23x–1.38x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;head_table&lt;/td&gt;
&lt;td&gt;4266.1–5298.0&lt;/td&gt;
&lt;td&gt;5167.9–6155.4&lt;/td&gt;
&lt;td&gt;1.14x–1.21x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rust is 1.90x to 4.29x faster on the four small calls, and 1.14x to 1.38x on the two table calls&lt;/strong&gt;, where the server itself does more work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers vary a lot between runs.&lt;/strong&gt; The Rust time for &lt;code&gt;list_namespaces&lt;/code&gt; ranged from 301.3 to 768.1 microseconds, a spread of 154.9%. That is why the table shows ranges.&lt;/p&gt;

&lt;p&gt;With no network involved, these ratios are as large as the difference can get. Step 4 adds the internet.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Break One Request Down
&lt;/h4&gt;

&lt;p&gt;The same &lt;code&gt;list_namespaces&lt;/code&gt; request is timed in stages on one &lt;code&gt;pyiceberg&lt;/code&gt; connection: the raw HTTP request, then parsing the JSON, then building Python objects, then the full client call. The same raw HTTP request is then timed from Rust.&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 bench_breakdown.py &lt;span class="nt"&gt;--iters&lt;/span&gt; 120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across seven runs on the local catalog, over two days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The raw HTTP request is 90% to 94% of &lt;code&gt;pyiceberg&lt;/code&gt;'s call time.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The same HTTP request takes &lt;strong&gt;269.8 to 479.7 microseconds&lt;/strong&gt; from Rust's HTTP library, &lt;code&gt;reqwest&lt;/code&gt;, and &lt;strong&gt;864.3 to 1286.4 microseconds&lt;/strong&gt; from Python's, &lt;code&gt;requests&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Everything &lt;code&gt;pyiceberg&lt;/code&gt; does after the HTTP request adds &lt;strong&gt;57.3 to 134.7 microseconds&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So 85% to 90% of the difference between the two clients is the HTTP library. That percentage is calculated from the two ranges above, run by run.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Small Differences Are Noise Here
&lt;/h4&gt;

&lt;p&gt;The breakdown tool prints, next to each stage, whether the time that stage added is bigger than the normal run-to-run noise. Only JSON parsing was. How the rest of &lt;code&gt;pyiceberg&lt;/code&gt;'s 57.3 to 134.7 microseconds splits up cannot be measured at this sample size.&lt;/p&gt;

&lt;p&gt;One setting is worth knowing about. &lt;code&gt;requests&lt;/code&gt; re-reads proxy settings from the environment on every call unless &lt;code&gt;trust_env=False&lt;/code&gt; is set. Turning it off saved between 32.9 and 89.9 microseconds in each of five runs, but that saving was bigger than the noise in only one run.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Time Them Over the Internet
&lt;/h4&gt;

&lt;p&gt;The same benchmark against BigLake and OneLake. Both clients use the same login token, created once before the run:&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 bench.py &lt;span class="nt"&gt;--only&lt;/span&gt; google-lakehouse &lt;span class="nt"&gt;--rounds&lt;/span&gt; 4 &lt;span class="nt"&gt;--iters&lt;/span&gt; 15 &lt;span class="nt"&gt;--storage&lt;/span&gt; opendal
&lt;span class="go"&gt;google-lakehouse   6 ops, 2 clients, 4 rounds x 15 iters = 720 samples
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 bench.py &lt;span class="nt"&gt;--only&lt;/span&gt; microsoft-onelake &lt;span class="nt"&gt;--rounds&lt;/span&gt; 4 &lt;span class="nt"&gt;--iters&lt;/span&gt; 15 &lt;span class="nt"&gt;--storage&lt;/span&gt; opendal
&lt;span class="go"&gt;microsoft-onelake  6 ops, 2 clients, 4 rounds x 15 iters = 720 samples
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two runs each, median time in milliseconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;op&lt;/th&gt;
&lt;th&gt;BigLake rust&lt;/th&gt;
&lt;th&gt;BigLake py&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;OneLake rust&lt;/th&gt;
&lt;th&gt;OneLake py&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;list_namespaces&lt;/td&gt;
&lt;td&gt;102.4–103.1&lt;/td&gt;
&lt;td&gt;92.2–106.7&lt;/td&gt;
&lt;td&gt;0.90x–1.04x&lt;/td&gt;
&lt;td&gt;36.0–36.4&lt;/td&gt;
&lt;td&gt;35.9–36.0&lt;/td&gt;
&lt;td&gt;0.99x–1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;load_namespace&lt;/td&gt;
&lt;td&gt;199.4–201.6&lt;/td&gt;
&lt;td&gt;199.1–200.9&lt;/td&gt;
&lt;td&gt;0.99x–1.01x&lt;/td&gt;
&lt;td&gt;44.6–45.1&lt;/td&gt;
&lt;td&gt;45.3–46.3&lt;/td&gt;
&lt;td&gt;1.02x–1.03x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;head_namespace&lt;/td&gt;
&lt;td&gt;198.4–200.4&lt;/td&gt;
&lt;td&gt;199.2–200.0&lt;/td&gt;
&lt;td&gt;0.99x–1.01x&lt;/td&gt;
&lt;td&gt;44.8–45.9&lt;/td&gt;
&lt;td&gt;45.3–46.6&lt;/td&gt;
&lt;td&gt;0.99x–1.04x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list_tables&lt;/td&gt;
&lt;td&gt;202.7–205.5&lt;/td&gt;
&lt;td&gt;203.2–206.1&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;td&gt;45.0–46.3&lt;/td&gt;
&lt;td&gt;45.4–45.6&lt;/td&gt;
&lt;td&gt;0.98x–1.01x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;load_table&lt;/td&gt;
&lt;td&gt;296.4–297.1&lt;/td&gt;
&lt;td&gt;281.9–284.3&lt;/td&gt;
&lt;td&gt;0.95x–0.96x&lt;/td&gt;
&lt;td&gt;36.3–39.0&lt;/td&gt;
&lt;td&gt;160.7–167.7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.12x–4.62x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;head_table&lt;/td&gt;
&lt;td&gt;201.7–205.0&lt;/td&gt;
&lt;td&gt;199.9–201.4&lt;/td&gt;
&lt;td&gt;0.98x–0.99x&lt;/td&gt;
&lt;td&gt;53.2–54.4&lt;/td&gt;
&lt;td&gt;53.4–53.8&lt;/td&gt;
&lt;td&gt;0.98x–1.01x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apart from &lt;code&gt;load_table&lt;/code&gt;, every ratio is between 0.90x and 1.04x. The local difference is under a millisecond per call, and a round trip of 36 to 200 milliseconds hides it.&lt;/p&gt;

&lt;p&gt;On BigLake, &lt;code&gt;pyiceberg&lt;/code&gt; was 4% to 5% faster on &lt;code&gt;load_table&lt;/code&gt; in both runs, for a reason this test did not identify.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: BigLake Allows 75 Catalog Requests a Minute
&lt;/h4&gt;

&lt;p&gt;BigLake limits catalog requests to 75 a minute per project, and answers faster bursts with HTTP 429. The benchmark discards any timing that did not return success, and &lt;code&gt;bench_breakdown.py --pace-ms&lt;/code&gt; waits between requests. The limit is visible with:&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;gcloud alpha services quota list &lt;span class="nt"&gt;--service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;biglake.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    --consumer=projects/&amp;lt;project&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --format="table(metric,consumerQuotaLimits[0].unit,consumerQuotaLimits[0].quotaBuckets[0].effectiveLimit)"
biglake.googleapis.com/irc_catalog_requests          1/min/{project}       75
biglake.googleapis.com/irc_read_requests             1/min/{project}       600
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — Compare the Two Clients' Requests
&lt;/h4&gt;

&lt;p&gt;OneLake's &lt;code&gt;load_table&lt;/code&gt; took 4.12x to 4.62x longer through &lt;code&gt;pyiceberg&lt;/code&gt;, while every other OneLake call was even. The two clients send different requests.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pyiceberg&lt;/code&gt; sends the header &lt;code&gt;X-Iceberg-Access-Delegation: vended-credentials&lt;/code&gt; by default (&lt;code&gt;catalog/rest/__init__.py:881&lt;/code&gt;). It asks the catalog to include a temporary storage credential with the table. The Rust client sends no such header.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bench_delegation.py&lt;/code&gt; times the same &lt;code&gt;pyiceberg&lt;/code&gt; request with and without the header:&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 bench_delegation.py &lt;span class="nt"&gt;--only&lt;/span&gt; microsoft-onelake &lt;span class="nt"&gt;--iters&lt;/span&gt; 40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;row                                  p50       p90
GET, header sent                   146.6     235.5
GET, header removed                 38.1      41.3
whole load_table                   148.5     182.4
whole load_table, no header         36.6      42.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the header, OneLake adds a storage credential to the response and takes 108.5 ms longer. Without it, &lt;code&gt;pyiceberg&lt;/code&gt;'s &lt;code&gt;load_table&lt;/code&gt; takes 36.6 ms, inside the Rust client's 36.3–39.0 ms. On Polaris and BigLake the header made no measurable difference.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;pyiceberg&lt;/code&gt; pays 108.5 to 111.9 ms and gets a credential for reading the table's files. The Rust client pays nothing and gets no credential. The companion article on the Rust client covers what happens when it then tries to read the files.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Measure Startup Time
&lt;/h4&gt;

&lt;p&gt;Startup here means starting a new process, connecting, and answering each of the six operations once. Median of five starts per client per run:&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;rust&lt;/th&gt;
&lt;th&gt;pyiceberg&lt;/th&gt;
&lt;th&gt;python takes longer by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Polaris, local, 4 runs&lt;/td&gt;
&lt;td&gt;16.4–26.0 ms&lt;/td&gt;
&lt;td&gt;525.8–557.9 ms&lt;/td&gt;
&lt;td&gt;507.2–535.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigLake, 2 runs&lt;/td&gt;
&lt;td&gt;1376.3–1410.3 ms&lt;/td&gt;
&lt;td&gt;1911.8–2016.7 ms&lt;/td&gt;
&lt;td&gt;535.5–606.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OneLake, 2 runs&lt;/td&gt;
&lt;td&gt;955.9–996.2 ms&lt;/td&gt;
&lt;td&gt;1723.5–1772.4 ms&lt;/td&gt;
&lt;td&gt;767.6–776.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Over the internet both clients wait on the same round trips, so the ratio drops from 21.4x–33.6x locally to 1.4x on BigLake and 1.8x on OneLake. &lt;strong&gt;The extra half second stays.&lt;/strong&gt; It matters for a CLI, a Lambda function or a short-lived agent, and hardly at all for a service that starts once.&lt;/p&gt;

&lt;p&gt;Where the half second goes, median of five, across seven local runs:&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;time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;start Python, &lt;code&gt;python3 -c pass&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;9.0–11.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import pyiceberg.catalog.rest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a further 337.9–366.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;connect: fetch the catalog config and log in&lt;/td&gt;
&lt;td&gt;6.8–12.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of it is importing the library. Connecting to the catalog is the smallest part.&lt;/p&gt;

&lt;p&gt;On OneLake the import took 338.9 and 341.4 ms, the same as locally, and fetching the config took 447.3 and 493.8 ms. Both clients fetch the config; only &lt;code&gt;pyiceberg&lt;/code&gt; pays for the import.&lt;/p&gt;




&lt;h4&gt;
  
  
  How This Compares to Published Lambda Numbers
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Cold Starts Are Dead&lt;/em&gt; measured AWS Lambda start times of &lt;strong&gt;88.3 ms for Python 3.13 on arm64&lt;/strong&gt; (106.2 ms on x86_64) and &lt;strong&gt;14.1 ms for Rust&lt;/strong&gt; (17.0 ms), at 512 MB, with hello-world functions and &lt;strong&gt;no libraries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The local Python startup here is about six times that: 525.8 to 557.9 ms against 88.3 ms. The difference is mostly the 337.9 to 366.3 ms spent importing &lt;code&gt;pyiceberg&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;🦀 iceberg-catalog-rest&lt;/th&gt;
&lt;th&gt;🐍 pyiceberg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;list_namespaces&lt;/code&gt;, local, µs&lt;/td&gt;
&lt;td&gt;🥇 301.3–768.1&lt;/td&gt;
&lt;td&gt;883.2–1479.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small calls over the internet&lt;/td&gt;
&lt;td&gt;tie, 0.90x–1.04x&lt;/td&gt;
&lt;td&gt;tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP library&lt;/td&gt;
&lt;td&gt;🥇 &lt;code&gt;reqwest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requests&lt;/code&gt;, 85%–90% of the difference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra startup time&lt;/td&gt;
&lt;td&gt;🥇 —&lt;/td&gt;
&lt;td&gt;507.2–776.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Of which, importing the library&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;337.9–366.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;load_table&lt;/code&gt; on OneLake, ms&lt;/td&gt;
&lt;td&gt;🥇 36.3–39.0, no credential&lt;/td&gt;
&lt;td&gt;160.7–167.7, with a storage credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests storage credentials&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;🥇 yes, by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints supported&lt;/td&gt;
&lt;td&gt;13 of 25&lt;/td&gt;
&lt;td&gt;🥇 21 of 25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;&lt;strong&gt;For a long-running service, pick by features.&lt;/strong&gt; Over the internet the per-call difference disappears, and startup happens once. Choose the client that supports the operations and login method you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a CLI, a Lambda function or a short-lived agent, startup decides it.&lt;/strong&gt; Python adds half a second or more per process on every catalog tested.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to measure how fast the Rust and Python Iceberg REST clients are. The key to the solution was running both as separate processes against the same catalogs, timing startup separately, and breaking one request down to see where the time goes. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Locally, Rust is 1.90x to 4.29x faster on small calls, and 85% to 90% of that difference is the HTTP library&lt;/li&gt;
&lt;li&gt;🟢 Over the internet the difference disappears: 0.90x to 1.04x on BigLake and OneLake&lt;/li&gt;
&lt;li&gt;⚠️ Python starts 507.2 to 776.2 ms slower on every catalog, mostly from importing the library (337.9 to 366.3 ms locally)&lt;/li&gt;
&lt;li&gt;⚠️ OneLake's 4x on &lt;code&gt;load_table&lt;/code&gt; comes from &lt;code&gt;pyiceberg&lt;/code&gt; requesting a storage credential by default, which the Rust client never does&lt;/li&gt;
&lt;li&gt;❌ &lt;code&gt;pyiceberg&lt;/code&gt; was 4% to 5% faster on BigLake's &lt;code&gt;load_table&lt;/code&gt;, cause unknown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: &lt;code&gt;iceberg-catalog-rest&lt;/code&gt; 0.10.1 in a release build with &lt;code&gt;rustc&lt;/code&gt; 1.98.1 and &lt;code&gt;rustls&lt;/code&gt; 0.23.45, and &lt;code&gt;pyiceberg&lt;/code&gt; 0.12.0 on Python 3.14.7 with &lt;code&gt;requests&lt;/code&gt; 2.34.2 over OpenSSL 3.5.7, on one 16-core Linux host. Apache Polaris 1.7.0 in Docker on the same machine with local file storage: four benchmark runs on 2026-09-17 and 2026-09-18, and seven breakdown runs of 80 to 120 requests each. Google BigLake and Microsoft OneLake over the internet on 2026-09-18: two benchmark runs each. Each benchmark run is 720 timed calls across six read operations; startup is the median of five starts per client per run. No writes were timed. Managed catalogs do not report a version, and each internet run is one region at one point in time.&lt;/p&gt;

&lt;p&gt;The strategy for comparing two Iceberg REST clients by speed was validated with an incremental step by step approach.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/lakehouse-iceberg-2026" rel="noopener noreferrer"&gt;lakehouse-iceberg-2026 | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust" rel="noopener noreferrer"&gt;apache/iceberg-rust&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-python" rel="noopener noreferrer"&gt;apache/iceberg-python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws/cold-starts-are-dead-5fod"&gt;Cold Starts Are Dead&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/querygraph/catalog-bench" rel="noopener noreferrer"&gt;querygraph/catalog-bench&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/seven-iceberg-rest-catalogs-what-they-declare-and-what-they-serve-40oj"&gt;Seven Iceberg REST Catalogs: What They Declare, and What They Serve&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>python</category>
      <category>iceberg</category>
      <category>performance</category>
    </item>
    <item>
      <title>What One Rust Client Can Reach Across Seven Iceberg Catalogs</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 25 Sep 2026 16:20:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-one-rust-client-can-reach-across-seven-iceberg-catalogs-24al</link>
      <guid>https://dev.to/aws-builders/what-one-rust-client-can-reach-across-seven-iceberg-catalogs-24al</guid>
      <description>&lt;p&gt;This article provides a step by step guide to pointing the Apache Rust client for Iceberg REST catalogs at seven catalogs and recording what works. A Python script runs a small Rust program against each catalog and saves every result.&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;h4&gt;
  
  
  What is this project trying to Do?
&lt;/h4&gt;

&lt;p&gt;An earlier article tested what seven Iceberg REST catalogs support. This one asks from the other side: if you write a lakehouse tool in Rust today, will it work with the catalog you already pay for, and what do you need to set up? The client is &lt;code&gt;iceberg-catalog-rest&lt;/code&gt; 0.10.1 from the Apache Iceberg project, and the catalogs, tests and test table are the same as in that article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apache Polaris&lt;/strong&gt; 1.7.0, running locally as the reference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google BigLake&lt;/strong&gt; and &lt;strong&gt;Microsoft OneLake&lt;/strong&gt;, over the internet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Glue&lt;/strong&gt; and &lt;strong&gt;AWS S3 Tables&lt;/strong&gt;, over the internet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks Unity&lt;/strong&gt; and &lt;strong&gt;Snowflake Horizon&lt;/strong&gt;, not run here&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a short one. This client implements 13 of the 25 operations the earlier article tested, it logs in to five of the seven catalogs, and on every catalog it logs in to, all 13 answer. Two lines of &lt;code&gt;Cargo.toml&lt;/code&gt; stand between it and those five. The other two are AWS, they require a signature this client cannot produce, and the Rust answer to that is two more catalog crates that do not implement quite the same operations.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does Each Result Mean?
&lt;/h4&gt;

&lt;p&gt;The test suite is 33 checks covering 25 of the 35 operations in the Iceberg REST specification. Each check gets one of five results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the client sent the request and got an answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the client sent the request and got an error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IMPLICIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the client sends this request on its own, and there is no way to call it directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT-EXPRESSIBLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the client has no method for this operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT-ISSUED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the client supports it, but it is a write, and that run only reads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  Where do I start?
&lt;/h4&gt;

&lt;p&gt;The strategy for testing the client is an incremental step by step approach.&lt;/p&gt;

&lt;p&gt;First, a local Polaris catalog is started and the client's source is read to list which operations it supports. The test program is then built and run against Polaris, reads first and writes after, then against the managed catalogs one login method at a time, and finally it tries to read a table's files.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Rust 1.94 or newer and &lt;code&gt;cargo&lt;/code&gt; — this run used &lt;code&gt;rustc&lt;/code&gt; 1.98.1&lt;/li&gt;
&lt;li&gt;Docker, for the local Polaris catalog&lt;/li&gt;
&lt;li&gt;Python 3.10+ with &lt;code&gt;pyyaml&lt;/code&gt;, for the test scripts&lt;/li&gt;
&lt;li&gt;Optional: logins for any managed catalog — &lt;code&gt;gcloud&lt;/code&gt; for BigLake, &lt;code&gt;az&lt;/code&gt; for OneLake, the AWS CLI for Glue and S3 Tables&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Start Polaris
&lt;/h4&gt;

&lt;p&gt;Polaris runs locally with permissive settings. Anything that fails here is a problem in the test scripts, so it is fixed before any cloud catalog is tested:&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;git clone https://github.com/xbill9/lakehouse-iceberg-2026
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;lakehouse-iceberg-2026/iceberg-conformance
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./polaris-up.sh
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;POLARIS_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root &lt;span class="nv"&gt;POLARIS_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;s3cr3t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 2 — Build the Test Program
&lt;/h4&gt;

&lt;p&gt;The versions are pinned exactly, so every result applies to one release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;iceberg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="py"&gt;"
iceberg-catalog-rest = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="py"&gt;"
iceberg-storage-opendal = { version = "&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;", features = ["&lt;/span&gt;&lt;span class="err"&gt;opendal-gcs&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="err"&gt;opendal-azdls&lt;/span&gt;&lt;span class="s"&gt;"] }&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;reqwest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;default-features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rustls-tls"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two lines add a TLS backend and the cloud storage backends. &lt;em&gt;What You Add Beyond the Client&lt;/em&gt;, near the end, says what happens without each of them.&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="nb"&gt;cd&lt;/span&gt; ../iceberg-rust-client
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;span class="go"&gt;    Finished `release` profile [optimized] target(s) in 1m 41s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 3 — List What the Client Supports
&lt;/h4&gt;

&lt;p&gt;Each of the 33 checks is matched to a method in the client's published source code, with the file and line number, and &lt;code&gt;check_refs.py&lt;/code&gt; confirms all 25 of those lines still sit inside the function they name. Counting each endpoint once, since five checks use the same &lt;code&gt;update_table&lt;/code&gt; endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;13 of 25 distinct endpoints are expressible through this client.
Counted per probe the figure is 19 of 33, which is the same fact
weighted by how many probes paper 1 happened to point at one endpoint.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 12 endpoints it cannot reach split two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing — 11 endpoints.&lt;/strong&gt; All seven view operations, scan planning, metrics reporting, the separate credentials endpoint, and &lt;code&gt;commitTransaction&lt;/code&gt;. The client builds eight URLs in total (&lt;code&gt;catalog.rs:177-215&lt;/code&gt;), and none of them is a view, a scan plan, a metrics report or a transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stubbed — 1 endpoint.&lt;/strong&gt; &lt;code&gt;Catalog::update_namespace&lt;/code&gt; exists, but returns the error &lt;code&gt;"Updating namespace not supported yet!"&lt;/code&gt; (&lt;code&gt;catalog.rs:659&lt;/code&gt;) and sends nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two endpoints it does reach, it reaches only in part, and both are counted among the 13: &lt;code&gt;load_table&lt;/code&gt; cannot ask for &lt;code&gt;?snapshots=all&lt;/code&gt;, and &lt;code&gt;list_namespaces&lt;/code&gt; handles paging internally and never sends &lt;code&gt;pageSize&lt;/code&gt;. It also reaches &lt;code&gt;registerTable&lt;/code&gt;, which is one of the ten operations the earlier article did not cover, so it sits outside this count of 25.&lt;/p&gt;

&lt;p&gt;Line numbers quoted here were read by hand and are archived, with the lines themselves, in &lt;code&gt;evidence/rust-source-citations.txt&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Run the Reads Against Polaris
&lt;/h4&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 run_rust.py &lt;span class="nt"&gt;--storage&lt;/span&gt; opendal
&lt;span class="go"&gt;apache-polaris     1 implicit, 14 not-expressible, 11 not-issued, 7 ok
wrote evidence/rust-run-apache-polaris.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 7 supported read operations work. The one &lt;code&gt;IMPLICIT&lt;/code&gt; result is the config request the client sends when it first connects, and the 11 &lt;code&gt;NOT-ISSUED&lt;/code&gt; are writes this run does not send.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Run the Writes Against Polaris
&lt;/h4&gt;

&lt;p&gt;The client has a method for each of those 11, and the read-only run sends none of them. A method that compiles can still fail on the wire, so this step sends them. Polaris is local, permissive and disposable, so the writes run there and on no other catalog, and every request goes through a small logging proxy that records what the client sent beside what came back:&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 run_writes.py
&lt;span class="go"&gt;apache-polaris     11 ok, 1 unsupported, 0 failed
wrote evidence/rust-write-surface.txt   22 request(s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  probe                          verdict      ms     endpoint
  create_namespace               OK           60     reachable
  update_namespace_props         UNSUPPORTED  0      unsupported
  create_table                   OK           64     reachable
  commit_table                   OK           112    reachable
  commit_remove_properties       OK           119    reachable
  commit_add_schema              OK           116    reachable
  commit_set_current_schema      OK           116    reachable
  commit_upgrade_format_version  OK           99     reachable
  rename_table                   OK           3      reachable
  drop_table_purge               OK           16     reachable
  drop_table                     OK           3      reachable
  drop_namespace                 OK           1      reachable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All eleven work, and the scratch namespace is dropped at the end. The one refusal is the stub from Step 3, and the proxy log shows why it takes 0 ms: no request to the properties endpoint appears anywhere in the run.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: The Proxy Log Shows Three Things
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;A properties commit always sends both update kinds.&lt;/strong&gt; Setting a property and removing one produce the same pair, &lt;code&gt;set-properties&lt;/code&gt; and &lt;code&gt;remove-properties&lt;/code&gt;, because the crate's properties action builds both every time (&lt;code&gt;update_properties.rs:95&lt;/code&gt;). The two differ in their contents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a column is one request carrying two updates&lt;/strong&gt;, with a requirement attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  POST    200  /v1/quickstart_catalog/namespaces/irc_probe_rust_1790009108/tables/t1
          updates: add-schema, set-current-schema
          requirements: assert-current-schema-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Every commit re-reads the table first&lt;/strong&gt;, so one property change is a &lt;code&gt;GET&lt;/code&gt; and then a &lt;code&gt;POST&lt;/code&gt;. That reload is also why a transaction commits against the table's current state, which is open upstream as &lt;code&gt;apache/iceberg-rust&lt;/code&gt; #3134.&lt;/p&gt;

&lt;p&gt;The log also confirms one thing: a two-level namespace goes out as &lt;code&gt;...%1Fchild&lt;/code&gt;, the unit separator the specification asks for.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Match the Login to Each Catalog
&lt;/h4&gt;

&lt;p&gt;The client can log in with a token, with an OAuth2 client ID and secret, or with fixed extra headers. It has no AWS request signing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;login&lt;/th&gt;
&lt;th&gt;catalogs&lt;/th&gt;
&lt;th&gt;with this client&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OAuth2&lt;/td&gt;
&lt;td&gt;Polaris&lt;/td&gt;
&lt;td&gt;built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token from an environment variable&lt;/td&gt;
&lt;td&gt;Unity&lt;/td&gt;
&lt;td&gt;built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key-pair JWT&lt;/td&gt;
&lt;td&gt;Horizon&lt;/td&gt;
&lt;td&gt;built in: a &lt;code&gt;credential&lt;/code&gt; with no colon is sent as &lt;code&gt;client_secret&lt;/code&gt; with no &lt;code&gt;client_id&lt;/code&gt; (&lt;code&gt;catalog.rs:238&lt;/code&gt;), which is what Horizon expects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gcloud&lt;/code&gt; or &lt;code&gt;az&lt;/code&gt; login&lt;/td&gt;
&lt;td&gt;BigLake, OneLake&lt;/td&gt;
&lt;td&gt;a token created outside the client, which the client cannot renew&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS SigV4 signing&lt;/td&gt;
&lt;td&gt;Glue, S3 Tables&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;not supported&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Glue and S3 Tables require SigV4 on the catalog requests themselves. The specification does not describe that — its security schemes are OAuth2 and bearer tokens, and the signing in it covers storage access — but &lt;code&gt;pyiceberg&lt;/code&gt; signs anyway, with &lt;code&gt;rest.sigv4-enabled&lt;/code&gt;, &lt;code&gt;rest.signing-name&lt;/code&gt; and &lt;code&gt;rest.signing-region&lt;/code&gt;, so the Python REST client reaches all seven catalogs and the Rust one reaches five. The request to add it, &lt;code&gt;apache/iceberg-rust&lt;/code&gt; #1236, has been open since April 2025.&lt;/p&gt;

&lt;p&gt;Fixed headers look like a way round it, and they are worth one measurement: a signature minted for &lt;code&gt;GET /v1/config&lt;/code&gt; and passed as a static header got a 200 on that request and a 403 on the next, on Glue and S3 Tables both, with all 7 checks refused through the client. A signature covers the request it signs.&lt;/p&gt;

&lt;p&gt;The token the client does hold is refreshed by &lt;code&gt;regenerate_token()&lt;/code&gt;, which only knows how to repeat an OAuth2 login, so a long-running program on BigLake or OneLake mints its own.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Reach AWS Through the Other Two Crates
&lt;/h4&gt;

&lt;p&gt;Rust does reach both AWS catalogs. The same Apache project publishes &lt;code&gt;iceberg-catalog-glue&lt;/code&gt; and &lt;code&gt;iceberg-catalog-s3tables&lt;/code&gt;, both 0.10.1, which call the AWS APIs directly and sign as the AWS SDK does. All three crates implement the same &lt;code&gt;Catalog&lt;/code&gt; trait, so a program written against &lt;code&gt;dyn Catalog&lt;/code&gt; swaps between them by changing a dependency.&lt;/p&gt;

&lt;p&gt;What changes with the dependency is the set of operations that answers. Reading each crate's &lt;code&gt;impl Catalog for&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Catalog trait method     rest       glue       s3tables
  update_namespace         refused    sent       refused
  drop_table               sent       sent       refused
  register_table           sent       sent       refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other 12 trait methods are sent by all three. Each refusal says why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  rest       update_namespace         'Updating namespace not supported yet!'
  s3tables   drop_table               'drop_table is not supported for S3Tables; use purge_table instead'
  s3tables   register_table           'Registering a table is not supported yet'
  s3tables   update_namespace         'Update namespace is not supported for s3tables catalog'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of those four is the service speaking: S3 Tables requires a purge, which the earlier article measured from the wire, so that refusal is the catalog's own rule carried faithfully by the crate. The other three are crate-level gaps, and they land at runtime, since all three crates satisfy the same trait and compile the same way.&lt;/p&gt;

&lt;p&gt;So a Rust tool covering all seven catalogs carries three catalog implementations and an operation set that varies by which one is loaded. In Python, one client covers all seven.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Point It at a Managed Catalog
&lt;/h4&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 run_rust.py &lt;span class="nt"&gt;--only&lt;/span&gt; google-lakehouse &lt;span class="nt"&gt;--only&lt;/span&gt; microsoft-onelake &lt;span class="nt"&gt;--storage&lt;/span&gt; opendal
&lt;span class="go"&gt;google-lakehouse   1 implicit, 14 not-expressible, 11 not-issued, 7 ok
wrote evidence/rust-run-google-lakehouse.json
microsoft-onelake  1 implicit, 14 not-expressible, 11 not-issued, 7 ok
wrote evidence/rust-run-microsoft-onelake.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same result as Polaris: all 7 supported read operations work on both, over the internet, with a token minted by &lt;code&gt;gcloud&lt;/code&gt; and by &lt;code&gt;az&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 9 — Read the Table's Files
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;load_table&lt;/code&gt; sets up file access but does not read anything, so each run also reads the table's metadata file through the client. The output lists the names of the settings the storage library received, never their values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apache-polaris
  fileio config keys: ...
  storage credential keys among them: none
  read: ok, scheme file, 5614 bytes, 21 ms
google-lakehouse
  fileio config keys: ...
  storage credential keys among them: none
  read: ok, scheme gs, 5497 bytes, 451 ms
microsoft-onelake
  fileio config keys: ...
  storage credential keys among them: none
  read: FAILED after 47066 ms
  error: Unexpected =&amp;gt; Failure in doing io operation, source: Unexpected
  (persistent) at read, context: { timeout: 10 } =&amp;gt; io timeout reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local files and Google Cloud Storage read fine, the second one on the Google login already on the machine. No catalog passed along a storage credential, because the client never asks for one: it sends no &lt;code&gt;X-Iceberg-Access-Delegation&lt;/code&gt; header, so the storage library finds a login on its own.&lt;/p&gt;

&lt;p&gt;Azure is where that runs out. The Azure backend, &lt;code&gt;opendal-service-azdls&lt;/code&gt; 0.57.0, has code to use your &lt;code&gt;az login&lt;/code&gt; and is never given a way to run the &lt;code&gt;az&lt;/code&gt; command (&lt;code&gt;backend.rs:297&lt;/code&gt;), which leaves an account key, a SAS token or a service principal secret — and OneLake has no account key. Asking for a credential does not change it: OneLake hands one out, and with the delegation header passed as a static header the read still timed out, after 47181 ms. The client reads the credential from the response (&lt;code&gt;types.rs:226&lt;/code&gt;) and copies only the response's &lt;code&gt;config&lt;/code&gt; settings into file access (&lt;code&gt;catalog.rs:455&lt;/code&gt;). Both halves are open upstream, as #2931 and #1442.&lt;/p&gt;




&lt;h4&gt;
  
  
  What You Add Beyond the Client
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;needed for&lt;/th&gt;
&lt;th&gt;what happens without it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a &lt;code&gt;reqwest&lt;/code&gt; TLS feature&lt;/td&gt;
&lt;td&gt;any &lt;code&gt;https://&lt;/code&gt; catalog&lt;/td&gt;
&lt;td&gt;every request fails before any response: 7 failed, 0 ok, no HTTP status. Upstream #2888&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg-storage-opendal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;loading any cloud-stored table&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;load_table&lt;/code&gt; refuses: &lt;em&gt;"StorageFactory must be provided for RestCatalog"&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a storage login in your environment&lt;/td&gt;
&lt;td&gt;reading any table file&lt;/td&gt;
&lt;td&gt;the read fails or hangs; catalog-issued credentials are read and unused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a SigV4 signer&lt;/td&gt;
&lt;td&gt;Glue and S3 Tables over REST&lt;/td&gt;
&lt;td&gt;no login at all, and Step 7's two crates are the way round it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first two are one line each. &lt;code&gt;iceberg-catalog-rest&lt;/code&gt; 0.10.1 declares &lt;code&gt;reqwest&lt;/code&gt; with TLS switched off and offers no feature to switch it on, so a build that never names a TLS backend fails on the first &lt;code&gt;https://&lt;/code&gt; request; Cargo merges features across dependencies, so your own line fixes it, and a project already using &lt;code&gt;reqwest&lt;/code&gt; with TLS will never see it. The core &lt;code&gt;iceberg&lt;/code&gt; 0.10.1 crate ships two storage factories, local files and memory (&lt;code&gt;io/storage/local_fs.rs:330&lt;/code&gt;, &lt;code&gt;io/storage/memory.rs:250&lt;/code&gt;), with its README pointing at the other crate on line 70.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;catalog&lt;/th&gt;
&lt;th&gt;login&lt;/th&gt;
&lt;th&gt;ok&lt;/th&gt;
&lt;th&gt;failed&lt;/th&gt;
&lt;th&gt;table files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache Polaris&lt;/td&gt;
&lt;td&gt;🟢 OAuth2&lt;/td&gt;
&lt;td&gt;7 reads, 11 writes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;🟢 read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google BigLake&lt;/td&gt;
&lt;td&gt;🟢 &lt;code&gt;gcloud&lt;/code&gt; token&lt;/td&gt;
&lt;td&gt;7 reads&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;🟢 read, on the machine's Google login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft OneLake&lt;/td&gt;
&lt;td&gt;🟢 &lt;code&gt;az&lt;/code&gt; token&lt;/td&gt;
&lt;td&gt;7 reads&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;❌ timed out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glue&lt;/td&gt;
&lt;td&gt;❌ SigV4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3 Tables&lt;/td&gt;
&lt;td&gt;❌ SigV4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks Unity&lt;/td&gt;
&lt;td&gt;token, not run&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake Horizon&lt;/td&gt;
&lt;td&gt;key-pair JWT, not run&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Behind those numbers every catalog that answered showed the same shape: 14 endpoints the client has no method for, and every endpoint it does implement returning an answer.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to point the Apache Rust Iceberg REST client at seven catalogs and record what works and what it takes. The key to the solution was matching every test to a line in the client's source code, running the same tests against each catalog, and logging every request the client sent. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Every endpoint the client implements answered on every catalog it could log in to: 7 reads on Polaris, BigLake and OneLake, and 11 writes on Polaris, with 0 failures&lt;/li&gt;
&lt;li&gt;🟢 13 of 25 endpoints are implemented; of the other 12, 11 are missing and 1 is a stub that sends no request&lt;/li&gt;
&lt;li&gt;⚠️ Two lines of &lt;code&gt;Cargo.toml&lt;/code&gt; stand in front of that: a TLS backend (upstream #2888) and &lt;code&gt;iceberg-storage-opendal&lt;/code&gt; for cloud storage&lt;/li&gt;
&lt;li&gt;❌ Glue and S3 Tables require SigV4, which this client cannot send, so a Rust tool covering all seven catalogs loads three catalog crates where Python loads one — and those three disagree on three of the 15 &lt;code&gt;Catalog&lt;/code&gt; methods, at runtime (upstream #1236, open since April 2025)&lt;/li&gt;
&lt;li&gt;❌ OneLake's files could not be read: the Azure backend cannot use an &lt;code&gt;az login&lt;/code&gt;, and a credential from the catalog is read and then ignored (upstream #2931, #1442)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: &lt;code&gt;iceberg-catalog-rest&lt;/code&gt; 0.10.1 with &lt;code&gt;iceberg&lt;/code&gt; 0.10.1, &lt;code&gt;iceberg-storage-opendal&lt;/code&gt; 0.10.1 and &lt;code&gt;reqwest&lt;/code&gt; 0.12.28 with &lt;code&gt;rustls-tls&lt;/code&gt;, built with &lt;code&gt;rustc&lt;/code&gt; 1.98.1. Source read 2026-09-04, versions captured 2026-09-17, catalog runs 2026-09-18, write run 2026-09-21, one run each from one machine in one region. Polaris 1.7.0 ran in Docker with permissive settings and local file storage, and the writes ran there alone, so those results describe one permissive server. Glue and S3 Tables were tested only with the signature experiment, and Unity and Horizon were not run at all, their rows coming from the source. The tests check that each operation answers; the answers themselves are not checked. Three of the seven catalogs were on trial accounts in the earlier article, and managed catalogs do not report a version.&lt;/p&gt;

&lt;p&gt;The strategy for testing what one Rust Iceberg client can reach was validated with an incremental step by step approach.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/lakehouse-iceberg-2026" rel="noopener noreferrer"&gt;lakehouse-iceberg-2026 | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust" rel="noopener noreferrer"&gt;apache/iceberg-rust&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust/issues/1236" rel="noopener noreferrer"&gt;iceberg-rust #1236 — REST catalog: support AWS sigV4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust/issues/2888" rel="noopener noreferrer"&gt;iceberg-rust #2888 — Add TLS features to iceberg-catalog-rest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust/issues/2931" rel="noopener noreferrer"&gt;iceberg-rust #2931 — Support refreshing vended storage credentials for REST catalog tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust/issues/1442" rel="noopener noreferrer"&gt;iceberg-rust #1442 — ADLS: Support vended "adls.sas-token.xxx" prefixed tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-rust/issues/3134" rel="noopener noreferrer"&gt;iceberg-rust #3134 — Transaction commits against a base it never validated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/iceberg-catalog-glue" rel="noopener noreferrer"&gt;iceberg-catalog-glue | crates.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/iceberg-catalog-s3tables" rel="noopener noreferrer"&gt;iceberg-catalog-s3tables | crates.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/seven-iceberg-rest-catalogs-what-they-declare-and-what-they-serve-40oj"&gt;Seven Iceberg REST Catalogs: What They Declare, and What They Serve&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>iceberg</category>
      <category>lakehouse</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>I Followed the n8n AWS Docs and It Broke at the First Command</title>
      <dc:creator>Sarvar Nadaf</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:09:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-followed-the-n8n-aws-docs-and-it-broke-at-the-first-command-4e1k</link>
      <guid>https://dev.to/aws-builders/i-followed-the-n8n-aws-docs-and-it-broke-at-the-first-command-4e1k</guid>
      <description>&lt;p&gt;You SSH into a fresh EC2 box, run &lt;code&gt;sudo dnf install -y docker&lt;/code&gt;, then the very next command from the guide you're following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it dies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker: &lt;span class="s1"&gt;'compose'&lt;/span&gt; is not a docker command.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hit this on camera. Followed the steps, watched n8n never come up, and spent a few minutes convinced I'd broken something. I hadn't. Almost every "self-host n8n on AWS" tutorial has this exact hole in it, because the authors tested on Ubuntu or a VPS where Docker installs differently. On Amazon Linux 2023, the default AMI for EC2, the command they tell you to run gives you half of what you need.&lt;/p&gt;

&lt;p&gt;This post is the fix, and the full path from a bare EC2 instance to your first n8n login. Two containers, one paste-ready compose file, no reverse proxy yet (that's the hardening step, and it's its own article). By the end you have n8n on Postgres running on a box you own.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gotcha, up front
&lt;/h2&gt;

&lt;p&gt;On Amazon Linux 2023:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;installs the Docker &lt;strong&gt;engine&lt;/strong&gt;. It does not install the &lt;strong&gt;Compose v2 plugin&lt;/strong&gt;. They're separate now. Compose stopped being a standalone &lt;code&gt;docker-compose&lt;/code&gt; binary years ago and became a plugin that lives under Docker's CLI, and &lt;code&gt;dnf&lt;/code&gt;'s docker package doesn't bundle it. So the engine runs fine, &lt;code&gt;docker run&lt;/code&gt; works, and then &lt;code&gt;docker compose&lt;/code&gt; throws &lt;code&gt;'compose' is not a docker command&lt;/code&gt; because the plugin isn't there.&lt;/p&gt;

&lt;p&gt;On Ubuntu you'd install &lt;code&gt;docker.io&lt;/code&gt; plus &lt;code&gt;docker-compose-plugin&lt;/code&gt; from Docker's apt repo and never notice. On AL2023 the plugin is on you. Here's the whole install, plugin included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /usr/local/lib/docker/cli-plugins
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/docker/compose/releases/download/v2.29.7/docker-compose-linux-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/lib/docker/cli-plugins/docker-compose
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/lib/docker/cli-plugins/docker-compose
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That third command is the one the other guides skip: it drops Docker's official Compose plugin binary into the directory the CLI actually looks in. Now both answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Docker version 25.0.14, build 0bab007&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose version
&lt;span class="c"&gt;# Docker Compose version v2.29.7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two version banners is how you know the box is actually ready. If you only check the first one, you find out the plugin is missing at &lt;code&gt;docker compose up&lt;/code&gt;, which is the worst time to find out.&lt;/p&gt;

&lt;p&gt;One catch on the download URL: it ends in &lt;code&gt;x86_64&lt;/code&gt;, which is right for a &lt;code&gt;t3&lt;/code&gt; (Intel) box. If you launched a Graviton/ARM instance (&lt;code&gt;t4g&lt;/code&gt; and friends), grab the &lt;code&gt;aarch64&lt;/code&gt; binary instead by swapping the filename to &lt;code&gt;docker-compose-linux-aarch64&lt;/code&gt;. The wrong architecture installs cleanly and then fails with an exec-format error the moment you run it, which sends you hunting in the wrong place. Pick the binary that matches your instance. And &lt;code&gt;v2.29.7&lt;/code&gt; is just the version I pinned here; check &lt;a href="https://github.com/docker/compose/releases" rel="noopener noreferrer"&gt;Docker's releases&lt;/a&gt; and use the current one if you'd rather not lag.&lt;/p&gt;




&lt;h2&gt;
  
  
  Launch the box first
&lt;/h2&gt;

&lt;p&gt;Before any of that, you need the instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AMI:&lt;/strong&gt; Amazon Linux 2023.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size:&lt;/strong&gt; &lt;code&gt;t3.small&lt;/code&gt; or larger. n8n plus Postgres want about 2 GB of RAM. Skip &lt;code&gt;t2.micro&lt;/code&gt; and &lt;code&gt;t3.micro&lt;/code&gt; (1 GB) unless you like watching containers get OOM-killed mid-run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Group:&lt;/strong&gt; SSH (22) from your IP only. For this private first test you can open 5678 to your IP only too. Do not open 5678 to &lt;code&gt;0.0.0.0/0&lt;/code&gt;. An open n8n editor on the public internet is an editor anyone can find and claim.&lt;/li&gt;
&lt;li&gt;SSH in: &lt;code&gt;ssh -i your-key.pem ec2-user@&amp;lt;PUBLIC_IP&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything below runs on that box as &lt;code&gt;ec2-user&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why bother self-hosting at all
&lt;/h2&gt;

&lt;p&gt;n8n is a credential aggregator. One instance can hold your Stripe key, your database password, your Slack token, your Google OAuth, all in one place. On n8n Cloud that pile lives on someone else's server, priced per seat, capped on executions. Self-hosting moves it onto a box inside your own AWS account: no seat fees, no execution caps, your data stays home, and you pick the version.&lt;/p&gt;

&lt;p&gt;The trade is honest. You now own the patching, the backups, and the security. This article gets it running. The hardening article (next in the series) closes the door behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack: two containers
&lt;/h2&gt;

&lt;p&gt;That's the whole thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; runs the editor and the workflow engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; stores workflows and executions so they survive a restart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not SQLite. n8n defaults to SQLite, which is fine for a five-minute look, but it handles concurrent executions poorly and migrating off it later is an afternoon you won't enjoy. Start on Postgres.&lt;/p&gt;

&lt;p&gt;Clone the repo so you have the compose file on the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/simplynadaf/self-host-n8n-on-ec2.git
&lt;span class="nb"&gt;cd &lt;/span&gt;self-host-n8n-on-ec2/compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&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;n8nio/n8n:1.123.64&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5678:5678"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_SECURE_COOKIE=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_DIAGNOSTICS_ENABLED=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PERSONALIZATION_ENABLED=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=change-me-to-a-long-random-string-please&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=postgresdb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=n8n&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=n8n&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=change-me-strong-db-password&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=n8n&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=n8n&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=change-me-strong-db-password&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines decide whether this works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The image is pinned to &lt;code&gt;1.123.64&lt;/code&gt;, not &lt;code&gt;latest&lt;/code&gt;.&lt;/strong&gt; Pinning makes the build reproducible, and this version patches a real issue: CVE-2026-65589, an info-disclosure bug where credentials passed as custom headers in LLM sub-nodes could land in execution records. Run &lt;code&gt;latest&lt;/code&gt; and you're one silent restart away from a version you didn't choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; encrypts every credential n8n stores.&lt;/strong&gt; Set a real 32-plus character random string and save it somewhere safe right now. Lose it and every saved credential is unrecoverable. A restored backup without this key is a database full of workflows whose logins can't be decrypted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DB password appears twice&lt;/strong&gt; (&lt;code&gt;DB_POSTGRESDB_PASSWORD&lt;/code&gt; in the n8n service, &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; in postgres) and the two values must match. If they don't, n8n can't reach its own database and the container just restarts in a loop while you wonder why the editor never loads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bring it up
&lt;/h2&gt;

&lt;p&gt;Edit the two passwords and the encryption key, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run pulls both images and starts them. Give it twenty to forty seconds, then check n8n is answering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'n8n -&amp;gt; HTTP %{http_code}\n'&lt;/span&gt; http://localhost:5678
&lt;span class="c"&gt;# n8n -&amp;gt; HTTP 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;200&lt;/code&gt; means n8n is up and serving. Once Compose was actually installed, this came back green on the first try.&lt;/p&gt;




&lt;h2&gt;
  
  
  First login
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;http://&amp;lt;PUBLIC_IP&amp;gt;:5678&lt;/code&gt; in a browser. A fresh instance shows the setup wizard. Create your owner account with an email and a strong password, submit, and you land on the canvas.&lt;/p&gt;

&lt;p&gt;Do this immediately after the box comes up, not tomorrow. The first account created on a fresh n8n becomes the &lt;strong&gt;owner&lt;/strong&gt;, and until someone submits that form, it's open to whoever reaches it first. On a private Security Group that's only you. It's still a habit worth keeping.&lt;/p&gt;

&lt;p&gt;One line in the compose file explains itself here: &lt;code&gt;N8N_SECURE_COOKIE=false&lt;/code&gt;. That's only so first login works over plain HTTP on a raw IP while testing. It's a development shortcut, not a keeper. In production n8n sits behind HTTPS and this setting goes away.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before this goes anywhere near the internet
&lt;/h2&gt;

&lt;p&gt;The compose here publishes port 5678 directly. That's fine for a private test where the Security Group only lets your IP in, but it falls apart on the open internet, where certificate transparency logs announce every new HTTPS host within minutes and scanners find fresh boxes fast.&lt;/p&gt;

&lt;p&gt;Before you point a domain at this or widen the Security Group:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep n8n patched (1.123.64 or newer).&lt;/li&gt;
&lt;li&gt;Stop publishing 5678. Use &lt;code&gt;expose&lt;/code&gt; so it's only reachable inside the Docker network.&lt;/li&gt;
&lt;li&gt;Put Caddy in front for automatic TLS, and restrict the editor to your admin IP. Leave only &lt;code&gt;/webhook/*&lt;/code&gt; public.&lt;/li&gt;
&lt;li&gt;Move the encryption key and DB password into AWS Secrets Manager, read through a least-privilege IAM role.&lt;/li&gt;
&lt;li&gt;Security Group: allow 22 (your IP), 80, 443. Never 5678 to &lt;code&gt;0.0.0.0/0&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That whole checklist is the hardening article in this series. If this box is going public, that's your next read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop and start without losing data
&lt;/h2&gt;

&lt;p&gt;Your data lives in the &lt;code&gt;pg_data&lt;/code&gt; volume, so you can stop the stack safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose down     &lt;span class="c"&gt;# stop, keep the data&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;    &lt;span class="c"&gt;# start again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;down&lt;/code&gt; stops the containers and keeps the volume, so your workflows and account are still there on the next &lt;code&gt;up&lt;/code&gt;. To also wipe the data, that's &lt;code&gt;down -v&lt;/code&gt;, and only when you mean it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you have now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;n8n running on an EC2 box you control, backed by Postgres.&lt;/li&gt;
&lt;li&gt;A pinned, patched image instead of a moving &lt;code&gt;latest&lt;/code&gt; target.&lt;/li&gt;
&lt;li&gt;An encryption key you actually set and saved.&lt;/li&gt;
&lt;li&gt;A clear line for what to do before this faces the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you know the one thing most AWS n8n guides get wrong: on Amazon Linux 2023, installing Docker does not install Compose, and the fix is one &lt;code&gt;curl&lt;/code&gt; into the plugin directory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the series goes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get it running&lt;/strong&gt; (this one): bare EC2 to first login, past the Compose gotcha.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harden it:&lt;/strong&gt; TLS, closed editor port, Secrets Manager, least-privilege IAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give it a brain:&lt;/strong&gt; wire this same n8n to Amazon Bedrock and build a real AI agent on the canvas, model running in your account, no OpenAI key anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The compose file, the install and verify scripts, and the full setup notes are in the repo: &lt;a href="https://github.com/simplynadaf/self-host-n8n-on-ec2" rel="noopener noreferrer"&gt;github.com/simplynadaf/self-host-n8n-on-ec2&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me for more on AWS architecture, DevOps, and AI Infrastructure:&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://sarvarnadaf.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/sarvar04/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://dev.to/sarvar_04"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://www.youtube.com/@sarvar-nadaf" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; | &lt;a href="mailto:simplynadaf@gmail.com"&gt;Email&lt;/a&gt; | &lt;a href="https://builder.aws.com/community/@sarvar" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt; | &lt;a href="https://x.com/SarvarN_04" rel="noopener noreferrer"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>n8n</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Fn::GetStackOutput — Référencer les outputs CloudFormation entre comptes AWS</title>
      <dc:creator>sylvainbruas</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:57:29 +0000</pubDate>
      <link>https://dev.to/aws-builders/fngetstackoutput-referencer-les-outputs-cloudformation-entre-comptes-aws-2pn1</link>
      <guid>https://dev.to/aws-builders/fngetstackoutput-referencer-les-outputs-cloudformation-entre-comptes-aws-2pn1</guid>
      <description>&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%2Fcq89d0i6psbzwbie4bbw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcq89d0i6psbzwbie4bbw.webp" alt="Fn::GetStackOutput — Référencer les outputs CloudFormation entre comptes AWS" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Dans un &lt;a href="https://sylvain.bruas.fr/blog/2025/04/sharedvpc" rel="noopener noreferrer"&gt;article précédent sur le VPC partagé&lt;/a&gt;, nous avons mis en place une architecture où le compte réseau possède et gère le &lt;a href="https://docs.aws.amazon.com/vpc/" rel="noopener noreferrer"&gt;Amazon Virtual Private Cloud (VPC)&lt;/a&gt; — subnets, tables de routage, NAT Gateways, &lt;a href="https://docs.aws.amazon.com/vpc/latest/tgw/" rel="noopener noreferrer"&gt;AWS Transit Gateway&lt;/a&gt; — et les autres comptes AWS (par équipe ou par projet) viennent y déployer leurs workloads en utilisant les subnets partagés via &lt;a href="https://docs.aws.amazon.com/ram/" rel="noopener noreferrer"&gt;AWS Resource Access Manager (RAM)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cette architecture est élégante du point de vue de la gouvernance, mais elle soulève immédiatement une question pratique : &lt;strong&gt;comment les templates &lt;a href="https://docs.aws.amazon.com/cloudformation/" rel="noopener noreferrer"&gt;AWS CloudFormation&lt;/a&gt; des équipes font-ils référence aux ressources du compte réseau ?&lt;/strong&gt; Le &lt;code&gt;VpcId&lt;/code&gt;, les &lt;code&gt;SubnetId&lt;/code&gt;, les security groups de base — tout ça vit dans le compte réseau, dans une stack que l'équipe réseau maintient.&lt;/p&gt;

&lt;p&gt;Jusqu'à très récemment, les options disponibles étaient toutes imparfaites : hard-coder les IDs (fragile), passer par &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="noopener noreferrer"&gt;AWS Systems Manager (SSM) Parameter Store&lt;/a&gt; (overhead opérationnel), ou utiliser &lt;code&gt;Fn::ImportValue&lt;/code&gt; (limité au même compte et même région). AWS vient de changer la donne avec &lt;code&gt;Fn::GetStackOutput&lt;/code&gt;, une nouvelle fonction intrinsèque qui résout exactement ce problème.&lt;/p&gt;


&lt;h2&gt;
  
  
  Le contexte : VPC partagé en multi-compte
&lt;/h2&gt;

&lt;p&gt;Rappelons rapidement l'architecture cible décrite dans &lt;a href="https://sylvain.bruas.fr/blog/2025/04/sharedvpc" rel="noopener noreferrer"&gt;l'article sur le VPC partagé&lt;/a&gt;. L'organisation AWS est découpée en plusieurs comptes :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compte Réseau&lt;/strong&gt; : propriétaire du VPC, des subnets, du Transit Gateway et de toute la couche réseau&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comptes Équipes&lt;/strong&gt; : chaque équipe (ou projet) a son propre compte AWS ; leurs ressources (&lt;a href="https://docs.aws.amazon.com/ec2/" rel="noopener noreferrer"&gt;Amazon Elastic Compute Cloud (EC2)&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/lambda/" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/rds/" rel="noopener noreferrer"&gt;Amazon Relational Database Service (RDS)&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;Amazon Elastic Container Service (ECS)&lt;/a&gt;…) sont déployées dans les subnets partagés depuis le compte réseau&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcq89d0i6psbzwbie4bbw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcq89d0i6psbzwbie4bbw.webp" alt="Architecture multi-comptes SharedVPC avec Fn::GetStackOutput" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Le compte réseau déploie une stack &lt;code&gt;NetworkStack&lt;/code&gt; qui contient toutes les ressources réseau et expose des &lt;strong&gt;Outputs&lt;/strong&gt; (VpcId, SubnetIds, NatGatewayId…). Les comptes équipes veulent utiliser ces valeurs dans leurs propres stacks CloudFormation.&lt;/p&gt;


&lt;h2&gt;
  
  
  Le problème : référencer des ressources cross-compte avant Fn::GetStackOutput
&lt;/h2&gt;

&lt;p&gt;Avant l'introduction de &lt;code&gt;Fn::GetStackOutput&lt;/code&gt;, trois approches coexistaient, chacune avec ses limitations :&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Hard-coding des IDs
&lt;/h3&gt;

&lt;p&gt;La solution la plus simple et la plus dangereuse. On récupère les IDs manuellement et on les copie dans les templates ou dans des fichiers de paramètres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Avant — fragile et non reproductible&lt;/span&gt;
&lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;VpcId&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;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vpc-0abc123def456789"&lt;/span&gt;  &lt;span class="c1"&gt;# ← IDs en dur&lt;/span&gt;
  &lt;span class="na"&gt;SubnetPrivateA&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;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subnet-0123456789abcdef0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le problème : si le compte réseau recrée le VPC (migration, incident, refactoring), tous les templates des équipes doivent être mis à jour manuellement. C'est une bombe à retardement.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SSM Parameter Store cross-compte
&lt;/h3&gt;

&lt;p&gt;L'équipe réseau stocke les valeurs dans SSM, et les équipes consomment via des SSM dynamic references.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Stack réseau — écriture dans SSM&lt;/span&gt;
&lt;span class="na"&gt;VpcIdParam&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;AWS::SSM::Parameter&lt;/span&gt;
  &lt;span class="na"&gt;Properties&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;/network/vpc-id&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;MyVpc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Stack équipe — lecture depuis SSM cross-compte (impossible nativement)&lt;/span&gt;
&lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{resolve:ssm:/network/vpc-id}}"&lt;/span&gt;  &lt;span class="c1"&gt;# ✗ ne fonctionne pas cross-compte&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les SSM dynamic references ne fonctionnent pas en cross-compte. L'équipe réseau devrait publier dans SSM &lt;strong&gt;dans chaque compte équipe&lt;/strong&gt;, ce qui génère un overhead opérationnel considérable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fn::ImportValue — limité au même compte
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Fn::ImportValue&lt;/code&gt; est la fonction native CloudFormation pour partager des outputs entre stacks. Elle crée une &lt;strong&gt;référence forte&lt;/strong&gt; (le stack producteur ne peut pas être supprimé tant qu'une stack consommatrice existe), mais elle est strictement limitée au &lt;strong&gt;même compte et même région&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ✗ Ne fonctionne pas cross-compte&lt;/span&gt;
&lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!ImportValue&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NetworkStack-VpcId"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aucune de ces approches ne résout proprement le cas du VPC partagé en multi-compte. C'est pour ça que &lt;code&gt;Fn::GetStackOutput&lt;/code&gt; est une avancée significative.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fn::GetStackOutput : la solution native
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Fn::GetStackOutput&lt;/code&gt; est une &lt;strong&gt;fonction intrinsèque CloudFormation&lt;/strong&gt; qui permet de référencer les outputs d'une autre stack, potentiellement dans un autre compte AWS ou une autre région. Elle résout la valeur &lt;strong&gt;au moment du déploiement&lt;/strong&gt; (create ou update), en appelant l'API &lt;code&gt;DescribeStacks&lt;/code&gt; de CloudFormation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Syntaxe
&lt;/h3&gt;

&lt;p&gt;La fonction accepte quatre paramètres :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paramètre&lt;/th&gt;
&lt;th&gt;Obligatoire&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StackName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oui&lt;/td&gt;
&lt;td&gt;Nom de la stack productrice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OutputName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oui&lt;/td&gt;
&lt;td&gt;Identifiant logique de l'output (pas le nom d'export)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non&lt;/td&gt;
&lt;td&gt;Région AWS de la stack productrice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RoleArn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non&lt;/td&gt;
&lt;td&gt;ARN du rôle IAM à assumer pour l'accès cross-compte&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Comment CloudFormation résout la valeur
&lt;/h3&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%2Fi6bsnak01fbcr9xuhmxa.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6bsnak01fbcr9xuhmxa.webp" alt="Flux de résolution de Fn::GetStackOutput en 5 étapes" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Le mécanisme est simple : au moment du déploiement, CloudFormation assume le rôle &lt;a href="https://docs.aws.amazon.com/iam/" rel="noopener noreferrer"&gt;AWS Identity and Access Management (IAM)&lt;/a&gt; spécifié dans le compte réseau, appelle &lt;code&gt;cloudformation:DescribeStacks&lt;/code&gt; sur la &lt;code&gt;NetworkStack&lt;/code&gt;, récupère la valeur de l'output demandé, et l'injecte dans le template en cours de déploiement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mise en pratique : templates CloudFormation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stack productrice — compte réseau
&lt;/h3&gt;

&lt;p&gt;L'équipe réseau déclare ses outputs normalement. &lt;strong&gt;Pas besoin de &lt;code&gt;Export&lt;/code&gt;&lt;/strong&gt; — c'est une différence importante avec &lt;code&gt;Fn::ImportValue&lt;/code&gt; qui requiert un nom d'export.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# NetworkStack — compte réseau (111111111111)&lt;/span&gt;
&lt;span class="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Infrastructure réseau partagée&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;SharedVpc&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;AWS::EC2::VPC&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CidrBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.0/16"&lt;/span&gt;
      &lt;span class="na"&gt;EnableDnsHostnames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;EnableDnsSupport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;Tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Name&lt;/span&gt;
          &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-vpc-production&lt;/span&gt;

  &lt;span class="na"&gt;SubnetPrivateA&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;AWS::EC2::Subnet&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SharedVpc&lt;/span&gt;
      &lt;span class="na"&gt;CidrBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.1.0/24"&lt;/span&gt;
      &lt;span class="na"&gt;AvailabilityZone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Select&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!GetAZs&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;SubnetPrivateB&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;AWS::EC2::Subnet&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SharedVpc&lt;/span&gt;
      &lt;span class="na"&gt;CidrBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.2.0/24"&lt;/span&gt;
      &lt;span class="na"&gt;AvailabilityZone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Select&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!GetAZs&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;SubnetPrivateC&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;AWS::EC2::Subnet&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SharedVpc&lt;/span&gt;
      &lt;span class="na"&gt;CidrBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.3.0/24"&lt;/span&gt;
      &lt;span class="na"&gt;AvailabilityZone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Select&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!GetAZs&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;Outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;du&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VPC&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;partagé"&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SharedVpc&lt;/span&gt;

  &lt;span class="na"&gt;SubnetPrivateA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subnet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;privé&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AZ&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;A"&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SubnetPrivateA&lt;/span&gt;

  &lt;span class="na"&gt;SubnetPrivateB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subnet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;privé&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AZ&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;B"&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SubnetPrivateB&lt;/span&gt;

  &lt;span class="na"&gt;SubnetPrivateC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subnet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;privé&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AZ&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;C"&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;SubnetPrivateC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notez l'absence de bloc &lt;code&gt;Export&lt;/code&gt; dans les outputs — &lt;code&gt;Fn::GetStackOutput&lt;/code&gt; référence directement le nom logique de l'output (&lt;code&gt;VpcId&lt;/code&gt;, &lt;code&gt;SubnetPrivateA&lt;/code&gt;…), pas un nom d'export.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stack consommatrice — compte équipe
&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;# AppStack — compte équipe A (222222222222)&lt;/span&gt;
&lt;span class="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application déployée dans le VPC partagé&lt;/span&gt;

&lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NetworkAccountId&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;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;111111111111"&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;AppSecurityGroup&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;AWS::EC2::SecurityGroup&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GroupDescription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SG&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pour&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;l'application"&lt;/span&gt;
      &lt;span class="na"&gt;VpcId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Fn::GetStackOutput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;StackName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkStack&lt;/span&gt;
          &lt;span class="na"&gt;OutputName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VpcId&lt;/span&gt;
          &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::${NetworkAccountId}:role/GetStackOutputRole"&lt;/span&gt;

  &lt;span class="na"&gt;AppLoadBalancer&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;AWS::ElasticLoadBalancingV2::LoadBalancer&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;application&lt;/span&gt;
      &lt;span class="na"&gt;Subnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Fn::GetStackOutput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;StackName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkStack&lt;/span&gt;
            &lt;span class="na"&gt;OutputName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SubnetPrivateA&lt;/span&gt;
            &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::${NetworkAccountId}:role/GetStackOutputRole"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Fn::GetStackOutput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;StackName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkStack&lt;/span&gt;
            &lt;span class="na"&gt;OutputName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SubnetPrivateB&lt;/span&gt;
            &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::${NetworkAccountId}:role/GetStackOutputRole"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Fn::GetStackOutput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;StackName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkStack&lt;/span&gt;
            &lt;span class="na"&gt;OutputName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SubnetPrivateC&lt;/span&gt;
            &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::${NetworkAccountId}:role/GetStackOutputRole"&lt;/span&gt;
      &lt;span class="na"&gt;SecurityGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;AppSecurityGroup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La syntaxe est identique pour chaque référence cross-compte. L'&lt;code&gt;OutputName&lt;/code&gt; est exactement le nom déclaré dans le bloc &lt;code&gt;Outputs&lt;/code&gt; de la stack réseau.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration IAM : le rôle GetStackOutputRole
&lt;/h2&gt;

&lt;p&gt;C'est la pièce centrale de la sécurité du dispositif. L'équipe réseau crée un rôle IAM dans son compte, que CloudFormation (depuis les comptes équipes) peut assumer pour lire les outputs.&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%2Ftb6wboxc064g0gso4lom.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftb6wboxc064g0gso4lom.webp" alt="Configuration IAM — GetStackOutputRole et trust policy cross-compte" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Voici le template CloudFormation pour créer le rôle IAM dans le &lt;strong&gt;compte réseau&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# À déployer dans le compte réseau — peut faire partie de NetworkStack&lt;/span&gt;
&lt;span class="na"&gt;GetStackOutputRole&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;AWS::IAM::Role&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RoleName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GetStackOutputRole&lt;/span&gt;
    &lt;span class="na"&gt;AssumeRolePolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17"&lt;/span&gt;
      &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
          &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;AWS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::222222222222:root"&lt;/span&gt;  &lt;span class="c1"&gt;# Compte Équipe A&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::333333333333:root"&lt;/span&gt;  &lt;span class="c1"&gt;# Compte Équipe B&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sts:AssumeRole&lt;/span&gt;
    &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DescribeNetworkStack&lt;/span&gt;
        &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17"&lt;/span&gt;
          &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
              &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudformation:DescribeStacks&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/NetworkStack/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Points importants sur ce rôle :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La permission &lt;code&gt;cloudformation:DescribeStacks&lt;/code&gt; est suffisante — pas besoin d'accès aux ressources réseau elles-mêmes&lt;/li&gt;
&lt;li&gt;Le &lt;code&gt;Resource&lt;/code&gt; est restreint à &lt;code&gt;NetworkStack&lt;/code&gt; uniquement, pas à toutes les stacks du compte réseau&lt;/li&gt;
&lt;li&gt;La trust policy liste explicitement les comptes autorisés — on peut affiner avec un &lt;code&gt;Principal&lt;/code&gt; par rôle d'exécution CloudFormation plutôt que par compte (&lt;code&gt;:root&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Référence forte vs référence faible
&lt;/h2&gt;

&lt;p&gt;C'est la différence fondamentale entre &lt;code&gt;Fn::ImportValue&lt;/code&gt; et &lt;code&gt;Fn::GetStackOutput&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;Caractéristique&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Fn::ImportValue&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Fn::GetStackOutput&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Portée&lt;/td&gt;
&lt;td&gt;Même compte + même région&lt;/td&gt;
&lt;td&gt;Cross-compte, cross-région&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nécessite un Export&lt;/td&gt;
&lt;td&gt;Oui&lt;/td&gt;
&lt;td&gt;Non&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type de référence&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Forte&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Faible&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suppression du stack producteur&lt;/td&gt;
&lt;td&gt;Bloquée&lt;/td&gt;
&lt;td&gt;Autorisée&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Propagation automatique des changements&lt;/td&gt;
&lt;td&gt;Non&lt;/td&gt;
&lt;td&gt;Non&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Une &lt;strong&gt;référence faible&lt;/strong&gt; signifie que si l'équipe réseau supprime ou recrée la &lt;code&gt;NetworkStack&lt;/code&gt;, CloudFormation ne l'en empêchera pas. Les stacks consommatrices ne le sauront pas immédiatement — elles échoueront uniquement lors de leur prochain déploiement.&lt;/p&gt;

&lt;p&gt;Pour compenser, deux mesures sont recommandées :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Activer la protection contre la suppression&lt;/strong&gt; sur &lt;code&gt;NetworkStack&lt;/code&gt; :
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;aws cloudformation update-termination-protection \&lt;/span&gt;
  &lt;span class="s"&gt;--enable-termination-protection \&lt;/span&gt;
  &lt;span class="s"&gt;--stack-name NetworkStack \&lt;/span&gt;
  &lt;span class="s"&gt;--region eu-west-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Documenter les dépendances&lt;/strong&gt; dans les READMEs et les runbooks — les références &lt;code&gt;Fn::GetStackOutput&lt;/code&gt; n'apparaissent pas dans la console CloudFormation du compte réseau.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Intégration avec AWS CDK
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cdk/" rel="noopener noreferrer"&gt;AWS Cloud Development Kit (CDK)&lt;/a&gt; supporte nativement &lt;code&gt;Fn::GetStackOutput&lt;/code&gt; pour les références cross-compte et cross-région. Avec la clé de contexte &lt;code&gt;@aws-cdk/core:defaultCrossStackReferences&lt;/code&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strong&lt;/code&gt; (défaut) : utilise &lt;code&gt;Fn::ImportValue&lt;/code&gt; pour les références dans le même compte/région&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weak&lt;/code&gt; : utilise toujours &lt;code&gt;Fn::GetStackOutput&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;both&lt;/code&gt; : génère les deux types simultanément (migration progressive)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pour notre architecture SharedVPC, le comportement &lt;code&gt;weak&lt;/code&gt; est le bon choix : on est obligatoirement en cross-compte.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cdk.json&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-cdk/core:defaultCrossStackReferences&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weak&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Ce qu'il faut retenir
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Fn::GetStackOutput&lt;/code&gt; permet de référencer les outputs d'une stack CloudFormation &lt;strong&gt;dans n'importe quel compte AWS et n'importe quelle région&lt;/strong&gt;, sans avoir besoin d'un nom d'export&lt;/li&gt;
&lt;li&gt;Pour le pattern &lt;strong&gt;VPC partagé&lt;/strong&gt;, c'est la solution idéale : l'équipe réseau expose ses outputs (VpcId, SubnetIds…), les équipes applicatives les consomment directement depuis leurs stacks&lt;/li&gt;
&lt;li&gt;La résolution se fait &lt;strong&gt;au moment du déploiement&lt;/strong&gt; via &lt;code&gt;cloudformation:DescribeStacks&lt;/code&gt; — un rôle IAM dans le compte réseau est nécessaire pour les accès cross-compte&lt;/li&gt;
&lt;li&gt;La référence est &lt;strong&gt;faible&lt;/strong&gt; (pas de blocage de suppression) — compensez avec la protection contre la suppression sur la &lt;code&gt;NetworkStack&lt;/code&gt; et une documentation explicite des dépendances&lt;/li&gt;
&lt;li&gt;La valeur est figée au moment du déploiement. Si l'équipe réseau modifie un output, les stacks consommatrices doivent être &lt;strong&gt;mises à jour explicitement&lt;/strong&gt; pour récupérer la nouvelle valeur&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Fn::GetStackOutput&lt;/code&gt; ne remplace pas &lt;code&gt;Fn::ImportValue&lt;/code&gt; pour les cas dans le même compte — la référence forte garde tout son intérêt quand on veut une garantie d'intégrité référentielle. Mais pour le cross-compte, c'est désormais l'option native et la plus propre.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudformation</category>
      <category>vpc</category>
      <category>multicompte</category>
    </item>
    <item>
      <title>Jev After Eight Days of Independent Tests: Level With Mid-Price LLMs, Behind the Frontier</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 24 Sep 2026 14:26:03 +0000</pubDate>
      <link>https://dev.to/aws-builders/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1c60</link>
      <guid>https://dev.to/aws-builders/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1c60</guid>
      <description>&lt;p&gt;This article provides a review of the independent evidence on TypeSafe's Jev, the open models built to replace it, and the prior art behind both, as of September 23, 2026. Every figure below is traced to a primary source, and re-scored from committed per-item outputs wherever the author published them. This is a snapshot eight days after launch, and the arXiv preprints it cites are days old and unrefereed.&lt;/p&gt;

&lt;p&gt;On accuracy, Jev sits level with mid-price LLMs and 6.5 to 11.5 points behind the frontier in the cleanest comparison. Out of the box its probabilities are the best calibrated of the models measured on familiar English tasks and are wrong in both directions off them, and one temperature fitted on 50 to a few hundred labels fixes most of the error.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  What Is Jev?
&lt;/h4&gt;

&lt;p&gt;Jev is a hosted model from TypeSafe AI, launched on September 15, 2026. It answers typed questions about a piece of text: &lt;code&gt;choice&lt;/code&gt; picks one option from a list, &lt;code&gt;score&lt;/code&gt; places the text on an ordered scale, and &lt;code&gt;noul&lt;/code&gt; returns the probability that a statement is true. It writes no text.&lt;/p&gt;

&lt;p&gt;The API is &lt;code&gt;POST /v1/systemone&lt;/code&gt; at &lt;code&gt;api.typesafe.ai&lt;/code&gt;. The price is $0.042 per million input tokens, and output is free. Access is through a waitlist, OpenRouter and Vercel's AI Gateway.&lt;/p&gt;

&lt;p&gt;TypeSafe calls this category a System One model, after Kahneman's fast and intuitive system. The company raised a $40M seed round led by DCVC, and its co-founders are Diogo Almeida (CEO), Sasha Sheng (COO) and Erik Gafni (CTO).&lt;/p&gt;

&lt;p&gt;The read itself is an old technique: end the prompt where the answer would start, take the model's scores for only the allowed label tokens, and apply a softmax over them. Any open model can be read this way; vLLM exposes it through &lt;code&gt;logprob_token_ids&lt;/code&gt;, SGLang through &lt;code&gt;/v1/score&lt;/code&gt;, and Featherless AI's SimpleJev wraps it as a Jev-compatible server.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Counts as Evidence Here?
&lt;/h4&gt;

&lt;p&gt;The review covers 14 arXiv preprints read in full, 104 GitHub evaluation repositories, 33 dev.to and Medium posts, TypeSafe's own pages and documentation, and press and forum coverage. Each figure carries one of three levels of support.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-scored:&lt;/strong&gt; recomputed from committed per-item outputs, and it matched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checked:&lt;/strong&gt; the source's arithmetic recomputed from its published tables, with no per-item data available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reported:&lt;/strong&gt; the author's statement, which cannot be checked from outside&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Studies are graded A (human or public labels, at least 200 items, stated method, committed outputs), B (real but small, synthetic, LLM-labelled or unreproducible) or C (a demo or a restatement).&lt;/p&gt;

&lt;p&gt;By that rubric, the posts split sharply. Of 33 dev.to and Medium posts read in full, 5 are careful measurements on human or public labels, 11 are small, synthetic or scored by agreement between models, and 17 restate vendor claims. Of the 104 GitHub repositories, 33 grade A, 57 grade B, 5 grade C, and 9 are tools. Of the 14 arXiv preprints, 10 call TypeSafe's hosted Jev.&lt;/p&gt;

&lt;p&gt;Who ran a study matters too. The studies with committed per-item outputs and no product in the race are Ibrahim and Zaki, Janardhan, sanand0, SamuelSacco and OmarMujahid, the last the most favourable to Jev; Rafe and Das also has none, and withholds its per-item outputs. Every's review was written by a launch partner. LangChain, which ships the &lt;code&gt;langchain-typesafe&lt;/code&gt; integration, reported 100% on 500 judgments that are 5 test cases repeated 100 times (not re-checked). And the "15.9% faster pipeline" headline came from a TypeSafe employee's three-case demo. Bespoke Labs sells Nimble, and the replica authors test their own models. Matt Mastracci wrote the DiffusionGemma PR he compares with Jev, and Google's Gemma account shared his thread. heiko-hotz, whose SNIPS comparison includes Gemma 4 31B, works at Google. synthorai, which found Jev behind flash LLMs, runs a gateway that sells access to the models it compared.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where Does Jev Land on Accuracy?
&lt;/h4&gt;

&lt;p&gt;Level with mid-price LLMs and behind the frontier, at a fraction of the frontier's price. That trade is the product's design, and the numbers below show its size.&lt;/p&gt;

&lt;p&gt;The largest pre-registered study, Ibrahim and Zaki's replication of a social-science annotation suite with 7,977 human-labelled items (arXiv 2609.24574), has Jev behind the best of 19 LLMs on 14 of 15 tasks, by a median 11.6 macro-F1 points. That comparator is the best model per task, picked after the results; within Jev's price band, Gemma 4 31B and Qwen3 235B score 3 and 2 F1 points above it. Its one per-item file checked, the 266-item dialect task, re-scores exactly to the paper's table.&lt;/p&gt;

&lt;p&gt;Manjunath Janardhan's 200-item, six-model comparison on BANKING77, BoolQ, Yelp and ChaosNLI re-scores exactly from its committed logs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Calibration error (ECE)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🥇 Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;84.0%&lt;/td&gt;
&lt;td&gt;0.064&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥈 GPT-6 Astra&lt;/td&gt;
&lt;td&gt;79.0%&lt;/td&gt;
&lt;td&gt;0.119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥉 DeepSeek V4.1 Flash&lt;/td&gt;
&lt;td&gt;76.0%&lt;/td&gt;
&lt;td&gt;0.138&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M3&lt;/td&gt;
&lt;td&gt;75.5%&lt;/td&gt;
&lt;td&gt;0.112&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K3&lt;/td&gt;
&lt;td&gt;74.5%&lt;/td&gt;
&lt;td&gt;0.119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jev&lt;/td&gt;
&lt;td&gt;72.5%&lt;/td&gt;
&lt;td&gt;0.161&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two frontier leads (+11.5 and +6.5 points) exclude zero in their 95% intervals. Kimi K3, MiniMax M3 and DeepSeek V4.1 Flash sit within noise of Jev.&lt;/p&gt;

&lt;p&gt;Banking77 is the one task many authors ran independently. Eight runs, one of them JevBench's own first-party run, put Jev at 0.753 to 0.840, with a median of 0.809. In every study that ran LLMs beside it, the best of them was ahead.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does Jev Do Well?
&lt;/h4&gt;

&lt;p&gt;Binary and few-class decisions, fast, with answers that always fit the schema.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spam:&lt;/strong&gt; 98.33% on 18,514 emails, level with TF-IDF logistic regression at 98.39%, and it held up on newer mail where the trained baseline fell to 72.5%; the question wording was tuned on labelled errors from the same sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reranking:&lt;/strong&gt; level with a dedicated reranker on eight retrieval sets, 0.692 against 0.691&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring code for backdoors:&lt;/strong&gt; AUROC of 0.976 and 0.970, catching about 90% of backdoors at 2% false positives; a best-of-5 attack cut detection to 60%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema compliance:&lt;/strong&gt; zero invalid answers across 23,703 decision-model calls, Jev's included, in the largest study&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability:&lt;/strong&gt; 1.33% of answers changed between identical passes in one study, 2.2% in the phishing benchmark, and 0 of 96 repeats in another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; German cost 0.5 points on MASSIVE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed and price:&lt;/strong&gt; about 0.1 s of server time, at $0.042 per million input tokens&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Are Its Probabilities Calibrated?
&lt;/h4&gt;

&lt;p&gt;They rank answers usefully; out of the box they beat most alternatives on familiar English tasks and miss badly off them.&lt;/p&gt;

&lt;p&gt;TypeSafe publishes no calibration error, reliability plot, Brier score or log loss for Jev on any dataset. Its only confidence-to-accuracy figure is one cookbook of 60 SEC filings: 90% right at confidence 0.9 or above, 40% below.&lt;/p&gt;

&lt;p&gt;The independent picture depends on the comparison.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Bespoke Labs' 13-subset public suite, Jev's median ECE is 0.071, lower than Nimble-9B's on 11 of 13 subsets, and lower than every Gemma 4 read in the companion article (0.114 to 0.180) before any fitting.&lt;/li&gt;
&lt;li&gt;Against LLMs that write their confidence as a number, Jev's own probabilities usually win. Its median ECE of 0.157 on the social-science tasks beats 16 of 19 LLMs.&lt;/li&gt;
&lt;li&gt;Once every model gets one fitted temperature, 15 of those LLMs beat it.&lt;/li&gt;
&lt;li&gt;Against LLMs that return full probability distributions, Jev's calibration error is the highest in Janardhan's run (0.161) and in sanand0's nine-model Banking77 pilot (0.138).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The direction of the error changes with the data. On public multi-class sets Jev is overconfident: on GoEmotions, labels it scored between 0.80 and 0.95 matched the human label 15% of the time. On crash narratives and synthetic items it is under-confident. Within one judge study, refit temperatures ran from 0.65 to 4.45, and its authors conclude that no single temperature fits.&lt;/p&gt;

&lt;p&gt;The API also rounds every probability to 0.01. In one committed sample, 70.4% of &lt;code&gt;choice&lt;/code&gt; probabilities came back as exactly 0, and &lt;code&gt;noul&lt;/code&gt; is clamped between 0.01 and 0.98.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Fit a Temperature on 50 to a Few Hundred of Your Own Labels
&lt;/h4&gt;

&lt;p&gt;A small refit fixes most of the calibration error, for Jev and for any open model read the same way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A two-parameter refit on civil_comments took Jev's ECE from 0.16–0.21 to under 0.025 on a held-out half&lt;/li&gt;
&lt;li&gt;Transferring a fitted slope and refitting only the intercept on 50 labels cut mean ECE by 74%&lt;/li&gt;
&lt;li&gt;Out-of-fold Platt scaling cut Jev's ECE on crash narratives by 3.35x&lt;/li&gt;
&lt;li&gt;In the companion article, one temperature fitted on 50 labels brought plain Gemma 4 26B's median ECE from 0.180 to 0.080 on the public suite, against Jev's 0.071 as shipped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The open replicas report the same order of correction: Nimble fitted 2.179, Luce 2.5 to 3.3 on unseen tasks. Luce's rule is to ship with 100 to 300 real labelled items and fit the temperature on them.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Much Faster and Cheaper Is It?
&lt;/h4&gt;

&lt;p&gt;Speed runs from 0.5x, slower than a local Gemma, to 12.1x faster; cost runs from 0.6x, dearer, to 478x cheaper, depending on the comparison model, the network path and where the clock runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Faster by&lt;/th&gt;
&lt;th&gt;Cheaper by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6 Luna, reasoning off, client clock&lt;/td&gt;
&lt;td&gt;OmarMujahid&lt;/td&gt;
&lt;td&gt;1.04x&lt;/td&gt;
&lt;td&gt;4.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek, Banking77&lt;/td&gt;
&lt;td&gt;sanand0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1.18x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5, phishing verdict&lt;/td&gt;
&lt;td&gt;jev-phishing-bench&lt;/td&gt;
&lt;td&gt;2.9x&lt;/td&gt;
&lt;td&gt;12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-size open model, 47 account reviews&lt;/td&gt;
&lt;td&gt;devopsdaily&lt;/td&gt;
&lt;td&gt;12.1x&lt;/td&gt;
&lt;td&gt;7.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1, via OpenRouter&lt;/td&gt;
&lt;td&gt;Janardhan&lt;/td&gt;
&lt;td&gt;9.9x&lt;/td&gt;
&lt;td&gt;478x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19 LLMs, social-science tasks (median)&lt;/td&gt;
&lt;td&gt;arXiv 2609.24574&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;18.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek, 6G edge orchestration&lt;/td&gt;
&lt;td&gt;arXiv 2609.23136&lt;/td&gt;
&lt;td&gt;1.3x&lt;/td&gt;
&lt;td&gt;0.6x (Jev dearer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek, edge orchestration, fees per correct completion&lt;/td&gt;
&lt;td&gt;arXiv 2609.22753, same lab&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;3.2–3.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Gemma 4 26B-A4B Q4 on a mini-PC&lt;/td&gt;
&lt;td&gt;ikkun1222&lt;/td&gt;
&lt;td&gt;0.5–0.65x (Jev slower)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server itself is fast: about 105 ms in one run, and about 76 ms once the network round trip is subtracted in another. Most of the cost gap comes from output being free. In devopsdaily's workload 83% of the old bill was output, and per input token the two prices differed by 1.31x. In the companion article, plain Gemma 4 26B on one EC2 L4 at full load cost at most $5.43 per million decisions, against Jev's $5.54 at the same 132-token prompts.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where Does It Break?
&lt;/h4&gt;

&lt;p&gt;On wording, language, and questions whose answer is absent from the input.&lt;/p&gt;

&lt;p&gt;Option names move answers. With the question, input, rubric and option set held fixed, swapping which rubric sits behind "no" and "yes" changed 32.5% of Jev's answers, against about 2% with neutral names, and AUROC fell from 0.81 to 0.58 (arXiv 2609.26758, n = 1,200; reported). The paper is graded C as evidence about Jev: it names no dataset or Jev version, its gold labels likely come from a teacher LLM, its code is unreleased, and no generative LLM was given the same swap.&lt;/p&gt;

&lt;p&gt;Bare labels mislead it. A router given option names with no descriptions sent all 40 hard tasks to the cheap model, at a median confidence of 0.96. One-line option descriptions fixed 37 of 40.&lt;/p&gt;

&lt;p&gt;Language shift costs accuracy. Russian XNLI dropped from 88.3% to 77.3% with ECE tripling, and Spanish cost 3 to 6 points.&lt;/p&gt;

&lt;p&gt;Jev is most confidently wrong where the answer does not follow from the input. With no "unknown" option, every answer on KoBBQ bias questions is wrong by design, and Jev gave them 0.79 confidence. On a fair die its &lt;code&gt;choice&lt;/code&gt; probabilities averaged 83% against a true 17%, while &lt;code&gt;noul&lt;/code&gt; gave 19%. On heart-risk data its probabilities ran about 3x too high, and a chat LLM on the same 5,000 people ranked risk slightly better (AUC 0.79 against 0.77).&lt;/p&gt;

&lt;p&gt;Questions batched in one request cannot see each other's answers. One reviewer's request returned "suspend" for an account the same response classified as a developer sending tests.&lt;/p&gt;

&lt;p&gt;TypeSafe's own list of known weaknesses names literal reading, arithmetic and counting, dates, long irrelevant input, adversarial content and phrasing: "refund" scored 0.72 and "not a refund" 0.47 on the same ticket.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does TypeSafe Claim, Checked?
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;193.6x faster and 444.6x cheaper.&lt;/strong&gt; The source is TypeSafe's four in-house workflow evals, which the launch post calls "on the higher end of real world gains". TypeSafe discloses that the workflows "were made by individuals on our model capabilities team, so some bias could exist", and that its LLM comparators ran through its own probability adapter, which it says is "slower and more expensive". The comparator is never named. Checked against the published summary points, 444.6x fits only Jev against Opus 5 in the workflow setup. Averaged over all eight workflow setups, the figures are 97.8x faster and 149.2x cheaper. Timings came from laptops on the US West Coast, with no case counts, run counts or variance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eval accuracy.&lt;/strong&gt; Reference labels are "an average of the responses of GPT-6 Astra and Claude Fable 5.1, both at high thinking", so accuracy there means agreement with two LLMs. Jev scores 67.8%, equal to Sonnet 5, with GPT-5.6 Sol leading at 74.1%. On invoice processing Jev scores 61.8% against Sol's 79.1%. TypeSafe states that it chose to publish no public-benchmark results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"0% hallucination".&lt;/strong&gt; TypeSafe's launch post: "Our number is not empirical. Schema matching is guaranteed." Every answer is a valid option; the wrong-but-valid rate is the accuracy figures above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RLCD.&lt;/strong&gt; Reinforcement learning for calibrated decisions has no paper, patent or method description. The CEO said on Hacker News that the architecture is "close to the chest for now, but we have talked about writing a paper." On Latent Space he agreed that all of Jev's training data is synthetic. Model size and base are undisclosed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing.&lt;/strong&gt; The launch post says "We can't prove it isn't subsidized", and the home-page FAQ says "We can serve Jev profitably at our current prices." How long the launch price holds is an open question for anyone building on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frontier comparison.&lt;/strong&gt; On Hacker News a commenter described Jev as "basically a zero-shot classifier" that classifies "as accurately (they claim) as a frontier-level LLM", and the CEO replied "exactly right!" The independent accuracy results above are the test of the second half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The founder.&lt;/strong&gt; TypeSafe's pages say Diogo Almeida "co-invented RLHF". He is not an author of Christiano et al. (2017), which introduced it; he is a primary author of InstructGPT (arXiv 2203.02155), which applied it to instruction following, and on Latent Space he separates the two.&lt;/p&gt;

&lt;p&gt;How the claims travelled:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim as circulated&lt;/th&gt;
&lt;th&gt;Primary source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;194x faster and 445x cheaper against GPT-6 Astra (Tom's Hardware)&lt;/td&gt;
&lt;td&gt;Astra is a reference labeller with no scored setup; 444.6x fits Opus 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage of arXiv 2609.26758: Jev flips 70.4 of 100 answers&lt;/td&gt;
&lt;td&gt;The paper gives 70.4 for an open ModernBERT head; Jev's is 32.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Its own employee measured 15.9% on a real pipeline" (Cherry Creek News)&lt;/td&gt;
&lt;td&gt;3 cases of a demo support ticket; costs modelled from an estimated price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Several self-published evaluations also state more than their own data supports, in both directions. The full report lists the corrections for Luce, Laya and NanoJev and uses the corrected figures from every study.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Predates Jev?
&lt;/h4&gt;

&lt;p&gt;Every piece of the mechanism.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Prior art&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Score options by label likelihood&lt;/td&gt;
&lt;td&gt;GPT-3 §2.4 (2020); MMLU §4.1 (2021); lm-evaluation-harness &lt;code&gt;multiple_choice&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map each label to one token&lt;/td&gt;
&lt;td&gt;Verbalizers, Schick &amp;amp; Schütze (arXiv 2001.07676)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label probabilities carry prior bias&lt;/td&gt;
&lt;td&gt;Calibrate Before Use, Zhao et al. (2021)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One fitted temperature fixes overconfidence&lt;/td&gt;
&lt;td&gt;Guo et al. (2017); Kadavath et al. (2022), a temperature of 2.5 for an RLHF policy, beside Jev's 2.66, Nimble's 2.179 and Luce's 2.5 to 3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-training worsens calibration&lt;/td&gt;
&lt;td&gt;GPT-4 report, Fig. 8: MMLU ECE 0.007 to 0.074 after PPO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-entropy rewards reporting true probabilities&lt;/td&gt;
&lt;td&gt;Gneiting &amp;amp; Raftery (2007): the log score is strictly proper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheap model first, defer when unsure&lt;/td&gt;
&lt;td&gt;Selective classification (2017); FrugalGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The visible contribution is the packaging: a typed API, one hosted endpoint, output priced at zero, and many questions evaluated against one input in a single request. Whether RLCD adds more can be judged once it is published; Jev's native probabilities beating most LLMs' self-reported confidence suggests the training does something.&lt;/p&gt;




&lt;h4&gt;
  
  
  What About the Open Alternatives?
&lt;/h4&gt;

&lt;p&gt;Each trained replica is built on Qwen or an encoder, and each publishes its own evaluation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Own headline&lt;/th&gt;
&lt;th&gt;Independent evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nimble-9B (Bespoke Labs)&lt;/td&gt;
&lt;td&gt;Qwen3.5-9B + LoRA&lt;/td&gt;
&lt;td&gt;75.9% vs Jev 77.3% on 13 public sets&lt;/td&gt;
&lt;td&gt;none; run by the seller, with raw probabilities (fitted temperature 2.179); its own model trails Jev&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kev (Jared Palmer)&lt;/td&gt;
&lt;td&gt;Qwen3.5 + LoRA + pointer head&lt;/td&gt;
&lt;td&gt;Kev-9B 0.852 test vs Jev 0.857 dev&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Luce (scienthoon)&lt;/td&gt;
&lt;td&gt;Qwen3-4B + LoRA&lt;/td&gt;
&lt;td&gt;86.9% vs Jev 84.7% on 500 kubernetes issues&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laya (Convai)&lt;/td&gt;
&lt;td&gt;ModernBERT-large 421M&lt;/td&gt;
&lt;td&gt;0.766 vs 0.727 after training on the benchmark&lt;/td&gt;
&lt;td&gt;ikkun1222: 29.2 / 60.0 / 40.0 on three Japanese tasks; the card's own zero-shot 0.362 sits below its 0.461 majority baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleJev (Featherless)&lt;/td&gt;
&lt;td&gt;any HF model; demo serves Gemma 4 26B&lt;/td&gt;
&lt;td&gt;no accuracy published&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DiffusionGemma via vLLM PR #57250&lt;/td&gt;
&lt;td&gt;one denoise step of &lt;code&gt;diffusiongemma-26B-A4B-it&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;198/201 vs Jev 191/201, "roughly tied"&lt;/td&gt;
&lt;td&gt;ywchiu: 32.2% vs Jev 61.4% on synthetic routing; companion article: 75.9% vs Jev 77.3% on the public suite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SimpleJev's README states that its probabilities "are not calibrated probabilities of correctness". The DiffusionGemma headline comes from 201 items built by the PR's author; publishing the items would let others reproduce it.&lt;/p&gt;

&lt;p&gt;Labels beat zero-shot wherever they were tried. A 310M Japanese encoder trained on 200 rows beat Jev on news topics by 12 points, 88.8% against 76.8%, and tied it on two sentiment tasks, and fine-tuned models led by 2 to 15 points on five public splits. On phishing, Luce trained on 1,000 labels reached 97.4% against Jev's 62.6% on the same benchmark, though on different items. On that benchmark, a two-line regex scored 91.6%, five narrow Jev questions combined by logistic regression reached 95.0% on a held-out half, and Haiku 4.5 asked the same five questions reached 93.2%, a difference too small to be significant (p = 0.063).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;thejevai.com&lt;/code&gt;, registered on September 20 and promoted on Hugging Face as the endpoint for API keys, has no stated connection to TypeSafe. TypeSafe's API is &lt;code&gt;api.typesafe.ai&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Jev&lt;/th&gt;
&lt;th&gt;Trained open replica&lt;/th&gt;
&lt;th&gt;Plain open model, label scores read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Labels needed&lt;/td&gt;
&lt;td&gt;none to run; about 50 to fit a temperature&lt;/td&gt;
&lt;td&gt;hundreds to thousands, plus a training run&lt;/td&gt;
&lt;td&gt;none to run; about 50 to fit a temperature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy on human labels&lt;/td&gt;
&lt;td&gt;level with Kimi K3, MiniMax M3 and DeepSeek V4.1 Flash; 6.5 to 11.5 behind the frontier&lt;/td&gt;
&lt;td&gt;trails or matches Jev generically; beats it when trained on the task's own labels&lt;/td&gt;
&lt;td&gt;Gemma 4 26B, 4-bit: 75.3% vs Jev 77.3% on the 3,880-record suite; level on yes/no, 4.5 behind on multiple choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calibration&lt;/td&gt;
&lt;td&gt;🥇 median ECE 0.071 as shipped on the public suite; none published by the vendor&lt;/td&gt;
&lt;td&gt;own evaluations&lt;/td&gt;
&lt;td&gt;median ECE 0.180 as shipped, 0.080 after 50 labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;TypeSafe, OpenRouter, Vercel&lt;/td&gt;
&lt;td&gt;your hardware&lt;/td&gt;
&lt;td&gt;your hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0.042 per million input tokens; $5.54 per million decisions at 132 tokens&lt;/td&gt;
&lt;td&gt;your hardware&lt;/td&gt;
&lt;td&gt;at most $5.43 per million decisions on an L4 at full load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;For binary and few-class decisions in English, at high volume, with no infrastructure to run and no labels yet, Jev is fast and cheap, its accuracy sits with mid-price LLMs, and its probabilities are the best calibrated out of the box on familiar English tasks. Plan on labelling 50 to a few hundred decisions to set thresholds and fit its probabilities.&lt;/p&gt;

&lt;p&gt;For multi-class work with many options, decisions where the rule lives outside the input, or non-English text, where Jev loses accuracy, a model trained on your own labels won wherever one was tried; for non-English that evidence is one Japanese study.&lt;/p&gt;

&lt;p&gt;For data that cannot leave your network, any open model read by label scores gives the same typed interface. In the companion article plain Gemma 4 26B trails Jev by 2.1 points overall and 4.5 on multiple choice, and needs about 50 labels to come within 0.01 of Jev's calibration.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Still Unmeasured?
&lt;/h4&gt;

&lt;p&gt;The published Gemma data points so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemma 4 31B beats Jev's median F1 in the pre-registered social-science study, 0.611 against 0.581&lt;/li&gt;
&lt;li&gt;Gemma 4 31B scores 98.86% on SNIPS against Jev's 97.14% after its output's code fences were stripped; 80.57% before&lt;/li&gt;
&lt;li&gt;Gemma 4 26B-A4B trails Jev by 11.6 points on Japanese news topics and ties it on two sentiment tasks&lt;/li&gt;
&lt;li&gt;Gemma 4 E4B, stating its confidence as text, scores 81.0% against Jev's 95.5% on synthetic emails&lt;/li&gt;
&lt;li&gt;Plain Gemma 4 26B read by its label probabilities, in the companion article: on four public tasks (sst2, AG News, DAIR Emotion, tweet_eval irony) and on Bespoke Labs' 3,880-record, 13-subset suite, 2.1 points behind Jev overall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still unpublished: Jev's behaviour across model versions, calibration across difficulty on real data, and whether a generative LLM shows the same option-name sensitivity.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to establish what independent evidence shows about TypeSafe's Jev and its open alternatives. The key to the solution was tracing every figure to its primary source and re-scoring from published per-item outputs wherever they existed. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Jev returns a valid option on every call, and its server time is about 0.1 s&lt;/li&gt;
&lt;li&gt;⚠️ Accuracy sits with mid-price LLMs: 72.5% against 74.5–76.0% for Kimi K3, MiniMax M3 and DeepSeek V4.1 Flash in one six-model study, and a median 11.6 F1 behind the best LLM per task in the largest pre-registered one&lt;/li&gt;
&lt;li&gt;🟢 Strong on binary and few-class work: 98.33% on 18,514 emails, and level with a dedicated reranker&lt;/li&gt;
&lt;li&gt;🟢 Best calibrated out of the box on familiar English tasks: median ECE 0.071 on the 13-subset public suite&lt;/li&gt;
&lt;li&gt;🟢 50 to a few hundred of your own labels and one fitted temperature fix most of its calibration error, and most of an open model's&lt;/li&gt;
&lt;li&gt;⚠️ Measured speed gains run from 0.5x to 12.1x and cost gains from 0.6x to 478x; TypeSafe's 193.6x and 444.6x have no named comparator&lt;/li&gt;
&lt;li&gt;⚠️ No calibration metric is published by TypeSafe, and the direction of its calibration error changes by domain&lt;/li&gt;
&lt;li&gt;⚠️ Swapping option names changed about a third of its answers in one unreplicated study&lt;/li&gt;
&lt;li&gt;⚠️ Plain Gemma 4 26B read by label probabilities (companion article) trails Jev by 2.1 points on the 3,880-record suite, is level on yes/no, 4.5 behind on multiple choice, and needs 50 labels to come within 0.01 of Jev's calibration&lt;/li&gt;
&lt;li&gt;❌ RLCD has no published method, and the model's size, base and training data are undisclosed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: sources read on September 23, 2026, eight days after launch: 14 arXiv preprints, 104 GitHub repositories, 33 dev.to and Medium posts, and TypeSafe's own pages; the companion article's public-suite run finished on September 24 UTC. Every accuracy, calibration and timing figure here is a third party's measurement, marked in the full report as re-scored, checked or reported, except the Gemma figures in Compare and Contrast and the Summary, which are the author's own measurement in the companion article; that article's Jev figures come from Bespoke Labs, which sells a competing model. Every run that states a version used &lt;code&gt;jev-1.13.0&lt;/code&gt;. Sources were gathered and audited with AI assistance (Claude), and every figure was checked against its source, cited in the full report, except LangChain's, marked not re-checked there. The author is a Google Developer Expert and an AWS Community Builder; neither Google nor AWS funded, reviewed or saw this work. The author has no relationship with TypeSafe, Bespoke Labs or any replica reviewed.&lt;/p&gt;

&lt;p&gt;The strategy for using primary sources to evaluate Jev was validated with an incremental step by step approach.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Full evidence review with every source: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/jev/reports/Jev%20independent%20evidence%20review.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/jev/reports/Jev%20independent%20evidence%20review.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion measurement article: &lt;a href="https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6"&gt;https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe launch post: &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;https://typesafe.ai/blog/introducing-system-one-models-and-jev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe models and pricing: &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe evals: &lt;a href="https://evals.typesafe.ai/" rel="noopener noreferrer"&gt;https://evals.typesafe.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Jev 1.13 known weaknesses: &lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/model-jaggedness/jev-1.13&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ibrahim &amp;amp; Zaki, arXiv 2609.24574: &lt;a href="https://arxiv.org/abs/2609.24574" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.24574&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rafe &amp;amp; Das, arXiv 2609.24052: &lt;a href="https://arxiv.org/abs/2609.24052" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.24052&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Li et al., arXiv 2609.26550: &lt;a href="https://arxiv.org/abs/2609.26550" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.26550&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Option naming, arXiv 2609.26758: &lt;a href="https://arxiv.org/abs/2609.26758" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.26758&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Janardhan, jev-frontier-bench: &lt;a href="https://github.com/manjunathshiva/jev-frontier-bench" rel="noopener noreferrer"&gt;https://github.com/manjunathshiva/jev-frontier-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Nimble public benchmarks: &lt;a href="https://github.com/bespokelabsai/nimble/blob/main/docs/PUBLIC_BENCHMARKS.md" rel="noopener noreferrer"&gt;https://github.com/bespokelabsai/nimble/blob/main/docs/PUBLIC_BENCHMARKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;jev-phishing-bench: &lt;a href="https://github.com/anisselbd/jev-phishing-bench" rel="noopener noreferrer"&gt;https://github.com/anisselbd/jev-phishing-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Luce: &lt;a href="https://github.com/scienthoon/luce" rel="noopener noreferrer"&gt;https://github.com/scienthoon/luce&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OmarMujahid, jev-decision-bench: &lt;a href="https://github.com/OmarMujahid/jev-decision-bench" rel="noopener noreferrer"&gt;https://github.com/OmarMujahid/jev-decision-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;bitnovus, jev-spam-eval: &lt;a href="https://github.com/bitnovus/jev-spam-eval" rel="noopener noreferrer"&gt;https://github.com/bitnovus/jev-spam-eval&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SamuelSacco, jev-exploration: &lt;a href="https://github.com/SamuelSacco/jev-exploration" rel="noopener noreferrer"&gt;https://github.com/SamuelSacco/jev-exploration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;sanand0, BANKING77 pilot: &lt;a href="https://sanand0.github.io/llmevals/jev/" rel="noopener noreferrer"&gt;https://sanand0.github.io/llmevals/jev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;heiko-hotz, jev-evaluation: &lt;a href="https://github.com/heiko-hotz/jev-evaluation" rel="noopener noreferrer"&gt;https://github.com/heiko-hotz/jev-evaluation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ywchiu, jev_benchmark: &lt;a href="https://github.com/ywchiu/jev_benchmark" rel="noopener noreferrer"&gt;https://github.com/ywchiu/jev_benchmark&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ikkun1222, Jev against a 310M encoder: &lt;a href="https://dev.to/ikkun1222/jev-vs-a-310m-encoder-i-trained-myself-750-rows-three-tasks-two-different-winners-242e"&gt;https://dev.to/ikkun1222/jev-vs-a-310m-encoder-i-trained-myself-750-rows-three-tasks-two-different-winners-242e&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SimpleJev: &lt;a href="https://github.com/featherless-ai/simple-jev" rel="noopener noreferrer"&gt;https://github.com/featherless-ai/simple-jev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM PR #57250: &lt;a href="https://github.com/vllm-project/vllm/pull/57250" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/pull/57250&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Matt Mastracci, Jev against DiffusionGemma: &lt;a href="https://x.com/mmastrac/status/2100626193943052784" rel="noopener noreferrer"&gt;https://x.com/mmastrac/status/2100626193943052784&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Guo et al., On Calibration of Modern Neural Networks: &lt;a href="https://arxiv.org/abs/1706.04599" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.04599&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kadavath et al., Language Models (Mostly) Know What They Know: &lt;a href="https://arxiv.org/abs/2207.05221" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2207.05221&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Brown et al., GPT-3: &lt;a href="https://arxiv.org/abs/2005.14165" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2005.14165&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hendrycks et al., MMLU: &lt;a href="https://arxiv.org/abs/2009.03300" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2009.03300&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schick &amp;amp; Schütze, verbalizers: &lt;a href="https://arxiv.org/abs/2001.07676" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2001.07676&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zhao et al., Calibrate Before Use: &lt;a href="https://arxiv.org/abs/2102.09690" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2102.09690&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gneiting &amp;amp; Raftery, Strictly Proper Scoring Rules, Prediction, and Estimation, Journal of the American Statistical Association 102 (2007)&lt;/li&gt;
&lt;li&gt;Geifman &amp;amp; El-Yaniv, Selective Classification: &lt;a href="https://arxiv.org/abs/1705.08500" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1705.08500&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chen et al., FrugalGPT: &lt;a href="https://arxiv.org/abs/2305.05176" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2305.05176&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GPT-4 technical report: &lt;a href="https://arxiv.org/abs/2303.08774" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2303.08774&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;InstructGPT: &lt;a href="https://arxiv.org/abs/2203.02155" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2203.02155&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Christiano et al. 2017: &lt;a href="https://arxiv.org/abs/1706.03741" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.03741&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Architecting an Enterprise Network on AWS Cloud WAN</title>
      <dc:creator>Kasun de Silva</dc:creator>
      <pubDate>Thu, 24 Sep 2026 04:31:22 +0000</pubDate>
      <link>https://dev.to/aws-builders/architecting-an-enterprise-network-on-aws-cloud-wan-14fi</link>
      <guid>https://dev.to/aws-builders/architecting-an-enterprise-network-on-aws-cloud-wan-14fi</guid>
      <description>&lt;p&gt;&lt;em&gt;How to design a multi-region, multi-account AWS network on Cloud WAN, from segments and attachments to single-hop inspection, and the lessons from running one in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most enterprise AWS networks grow the same way. One VPC becomes fifty. One region becomes three or four. A Transit Gateway appears in each region, they get peered, and a team spends a growing share of its week keeping route tables in agreement across all of them. It works, and it gets a little more brittle every quarter.&lt;/p&gt;

&lt;p&gt;AWS Cloud WAN is the answer to that pattern. I have run it in production for a network of a few hundred accounts and several hundred VPCs across four regions, and this post is the guide I wish I had at the start: how regions connect, how segments carve up the network, how inspection works, how to attach VPCs at scale, and what to expect operationally. It is a design guide, not a tutorial. There is no console clicking here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem Cloud WAN solves
&lt;/h2&gt;

&lt;p&gt;A Transit Gateway is regional. Run in four regions and you run four of them, peer them in a mesh, and keep the route tables on all four telling a consistent story. Every new VPC is an attachment, a route table association, propagation into the right tables, and often a static route or two. Every new environment or partner multiplies that.&lt;/p&gt;

&lt;p&gt;Two problems dominate at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing intent is spread everywhere.&lt;/strong&gt; No single document says "test can reach shared services, and nothing else." That intent lives in route tables in four regions, and you prove it by reading all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspection is double-hop.&lt;/strong&gt; Traffic between VPCs in different regions goes through a firewall leaving the source region and another entering the destination region. Stateful firewalls need to see both directions of a flow, and with independent regional Transit Gateways the only way to guarantee symmetry is to inspect at both ends. Twice the traversals, twice the logs, two rule sets that must agree.&lt;/p&gt;

&lt;p&gt;Cloud WAN removes both problems, and that is the reason to adopt it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud WAN in one paragraph
&lt;/h2&gt;

&lt;p&gt;Cloud WAN gives you a &lt;strong&gt;global network&lt;/strong&gt;, which holds a &lt;strong&gt;core network&lt;/strong&gt; that AWS operates for you. The core network has an &lt;strong&gt;edge&lt;/strong&gt; in each region you choose, and AWS connects those edges over its backbone. You attach VPCs, Transit Gateways, Direct Connect gateways and VPNs to the edges. Every attachment lands in a &lt;strong&gt;segment&lt;/strong&gt;, a routing domain that spans all edges. The whole thing, every segment, every rule about which segment can reach which, and where traffic gets inspected, is described in one versioned JSON document: the &lt;strong&gt;core network policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Transit Gateway is a router you configure, Cloud WAN is a WAN you describe.&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%2Frsnam4nlzzed8poo47su.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%2Frsnam4nlzzed8poo47su.png" alt=" " width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the diagram
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Columns are regional edges.&lt;/strong&gt; Everything inside a column is attached to that edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows are segments.&lt;/strong&gt; A segment exists on every edge, so cards in the same row reach each other across regions for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green links&lt;/strong&gt; are the shared services hub reaching into each environment. Two-way, never transitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashed orange arrows&lt;/strong&gt; are internet egress. Every VPC drains to its region's inspection VPC, which holds the NAT and internet gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solid orange paths&lt;/strong&gt; are two production flows to another cloud, inspected once each and exiting through the nearest Direct Connect landing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The VPNs have no arrows&lt;/strong&gt; because their path is the same as the orange one, with the VPN in place of the gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the post explains each of these in turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regions: edges, not peerings
&lt;/h2&gt;

&lt;p&gt;Each region you list in the policy becomes a core network edge. AWS peers every edge to every other edge. You never create an inter-region peering attachment, and you never maintain inter-region routes. When a VPC attaches in Sydney, its CIDR is learned by the edges in Virginia and Oregon automatically, inside whichever segment it joined. Inter-region routing is just intra-segment routing.&lt;/p&gt;

&lt;p&gt;The default behaviour is region affinity. Traffic stays in its home region wherever it can and crosses the backbone only when the destination or a policy says so. That gives predictable paths by default, and you override it deliberately when you want a particular region to handle something, such as inspection or egress.&lt;/p&gt;

&lt;p&gt;An edge is a router you never see. Every attachment in a region terminates on it, it holds one route table per segment, and AWS peers it to every other edge. It has no internet gateway and no firewall of its own. Anything that needs either has to be a VPC you attach to it, which is why the inspection VPCs carry both jobs below.&lt;/p&gt;

&lt;p&gt;Each edge needs its own ASN from a range you reserve in the policy. Pick a private range, keep it out of anything you peer with, and never change it afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Segments: the routing intent, written down
&lt;/h2&gt;

&lt;p&gt;Segments are where the design lives. A segment is a routing domain that exists on every edge. Attachments in the same segment can reach each other across regions. Attachments in different segments cannot, unless the policy shares routes between them.&lt;/p&gt;

&lt;p&gt;An enterprise segment model that works well has four groups.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;One segment each for test, staging and production workloads&lt;/td&gt;
&lt;td&gt;Isolated. A workload segment sees itself and shared services, nothing else.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared services&lt;/td&gt;
&lt;td&gt;Central logging, DNS, egress, platform tooling&lt;/td&gt;
&lt;td&gt;Shared into every environment segment across all regions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External&lt;/td&gt;
&lt;td&gt;VPNs, SaaS platforms and partners that do not fit the internal pattern&lt;/td&gt;
&lt;td&gt;One per environment. Isolated, so external parties never see each other.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inter-cloud&lt;/td&gt;
&lt;td&gt;Footprints in other public clouds&lt;/td&gt;
&lt;td&gt;One per environment. Isolated, routed through inspection.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three rules make this model hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared services is a hub, not a bridge.&lt;/strong&gt; Every environment segment can reach shared services, but sharing is not transitive. Test cannot reach production by going through shared services. The policy shares one segment's routes into another explicitly, and you only share in one direction: into the hub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split external and inter-cloud by environment.&lt;/strong&gt; One external segment is simpler. Three means a test VPN endpoint can never be a path into production, and inspection rules for each environment stay separate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use segment isolation for anything third-party.&lt;/strong&gt; Isolation is a segment attribute. Set it and attachments inside the segment cannot reach each other, only what you share in. Every partner VPN becomes an island that reaches exactly what you routed to it.&lt;/p&gt;

&lt;p&gt;Written out, the reachability of the whole network fits in one table. This is what the policy encodes, and it applies identically on every edge.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Can reach&lt;/th&gt;
&lt;th&gt;Isolated&lt;/th&gt;
&lt;th&gt;Inspected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test&lt;/td&gt;
&lt;td&gt;Test and shared services, all regions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Egress only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Staging and shared services, all regions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Egress only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Production and shared services, all regions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Egress only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared services&lt;/td&gt;
&lt;td&gt;Every environment segment, all regions&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Egress only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External (per environment)&lt;/td&gt;
&lt;td&gt;Its own environment, via inspection&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Every flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inter-cloud (per environment)&lt;/td&gt;
&lt;td&gt;Its own environment, via inspection&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Every flow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The segment count stays small. Twelve to fifteen segments covers most enterprises. If you find yourself creating a segment per team, you are rebuilding route tables under a new name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attachments: make joining the network boring
&lt;/h2&gt;

&lt;p&gt;At a few hundred accounts, attaching a VPC has to be self-service or your network team becomes a ticket queue. The pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Share the core network to every member account with AWS Resource Access Manager. One share, one organisational unit.&lt;/li&gt;
&lt;li&gt;Let the VPC owner create the attachment in their own account, through whatever internal platform or Terraform module they already use.&lt;/li&gt;
&lt;li&gt;Have the attachment carry a tag naming its segment. The tag is the only thing the owner controls.&lt;/li&gt;
&lt;li&gt;Write an attachment policy in the core network policy that matches the tag and places the attachment in the segment. No human chooses a segment for a VPC.&lt;/li&gt;
&lt;li&gt;Let route propagation do the rest. The VPC's CIDR appears in the segment's route table on every edge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Turn attachment acceptance off for internal segments. If your network team manages both the core network and the accounts that attach to it, a manual approve step adds friction and no security, because segment placement is already decided by the policy, not by whoever created the attachment. Keep acceptance on for external segments where a third party could be on the other end.&lt;/p&gt;

&lt;p&gt;Enable appliance mode on attachments to inspection VPCs, and only those. Appliance mode pins both directions of a flow to the same availability zone, which stateful firewalls need. Workload VPCs do not need it and it costs you a cross-AZ hop if you turn it on everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspection: one hop, not two
&lt;/h2&gt;

&lt;p&gt;An inspection VPC is a VPC with a stateful firewall in it, attached to the core network, that traffic is steered through. Cloud WAN steers that traffic with &lt;strong&gt;network function groups&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You put inspection VPCs into a group, then in the policy you say which segment-to-segment flows must go &lt;em&gt;via&lt;/em&gt; that group. That is a send-via rule, and it is how you inspect east-west traffic between environments, or between an environment and a partner. There is also send-to, which sends a segment's traffic &lt;em&gt;to&lt;/em&gt; a group, typically a default route, and is how you do centralised egress.&lt;/p&gt;

&lt;p&gt;Egress deserves a sentence of its own because it surprises people. Every segment, shared services included, gets a send-to rule that installs a default route towards the group, and the inspection VPC holds the NAT and internet gateway. Nothing else on the network has a way out. A workload VPC with its own NAT gateway is a hole in the design, so do not build one.&lt;/p&gt;

&lt;p&gt;The win is single-hop inspection. Because the core network sees the whole flow end to end, it can send both directions through the same firewall and keep the flow symmetric even when source and destination are in different regions. An inter-region flow crosses one firewall, not two. Fewer traversals, less log volume, one rule set to reason about, and far simpler troubleshooting.&lt;/p&gt;

&lt;p&gt;Which region's firewall handles a cross-region flow? The policy decides. You can prefer the region closest to the source, set an explicit priority order, or override per edge. That lets you route a small region's traffic through a neighbour's firewall until the small region justifies its own, and fail over when a firewall needs maintenance.&lt;/p&gt;

&lt;p&gt;Design the groups per environment. One group for production inspection, one for lower environments, one for egress. Sharing a firewall between production and test saves money and blurs a boundary you spent the segment model building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid and multi-cloud: same edges, same segments
&lt;/h2&gt;

&lt;p&gt;Everything that is not a VPC attaches to the same core network and lands in a segment like anything else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Other clouds&lt;/strong&gt; connect over a Direct Connect gateway attachment. Many enterprises deliver those circuits through a connectivity provider rather than dedicated fibre at every site. Land them in the inter-cloud segments and send all their traffic via inspection. Land the same cloud in two regions. Both gateways advertise the same prefixes into the segment, each edge prefers the nearer one, and if a circuit drops its routes withdraw and every edge converges on the other. The two gateways never talk to each other, and a VPC in Oregon does not cross the Pacific to reach a cloud it can exit to locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPNs and partners&lt;/strong&gt; land in external segments, one per environment, each isolated so partners never share a routing domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing Transit Gateways&lt;/strong&gt; can attach as route table attachments. That is a bridge, not a native attachment type, and it is how you keep older things running while you migrate them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is one place to look. A partner VPN, a link to another cloud and a production VPC are rows in the same attachment list, placed by the same policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from Transit Gateway without a big bang
&lt;/h2&gt;

&lt;p&gt;Cloud WAN and Transit Gateway coexist well, and that is the migration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build the core network beside the Transit Gateways.&lt;/strong&gt; Peer each regional Transit Gateway to its local edge. The Transit Gateways' prefixes propagate into Cloud WAN, so Cloud WAN already knows how to reach everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach VPCs without moving traffic.&lt;/strong&gt; Each VPC gets a Cloud WAN attachment and its CIDR propagates, but its route tables still point at the Transit Gateway. Verify the routes are learned on every edge before going further.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut over with managed prefix lists.&lt;/strong&gt; Add Cloud WAN routes to VPC route tables using a prefix list, so a growing set of destinations moves to Cloud WAN while the Transit Gateway stays the default. You move routes per environment, per region, and roll back by editing a prefix list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flip the default.&lt;/strong&gt; Once a VPC's traffic is all on Cloud WAN, point its default route at the core network and remove the Transit Gateway attachment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Go test, then staging, then production. Run Route Analyzer before and after every step to confirm paths, and run synthetic connectivity tests across attachments to catch what Route Analyzer cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operating it: what to know before you commit
&lt;/h2&gt;

&lt;p&gt;The design above is the good news. These are the things that will shape your day-to-day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy changes take minutes.&lt;/strong&gt; Applying a new policy version, and rolling one back, takes minutes rather than seconds. On a core network carrying production that is a long time to watch a change land. Test in a lower environment first, and know exactly which version you are rolling back to before you press the button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything is one document.&lt;/strong&gt; That is the strength and the hazard. A mistake in one attachment rule affects every region at once. Treat the policy like production code: Terraform, pull requests, plan output reviewed, applied by a pipeline, never edited in the console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The console diff gets noisy.&lt;/strong&gt; A large policy does not always make a small change look small in the change view. Review the diff in source control first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions cut across accounts and services.&lt;/strong&gt; The roles for the team that runs the core network, and for the automation that creates attachments in member accounts, take real least-privilege work. Start that with your security team early, not the week before production cutover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability is on you.&lt;/strong&gt; Cloud WAN gives you route tables, attachment state and a Route Analyzer. It does not give you flows. Ship VPC flow logs from every attachment and every inspection VPC into one queryable store. That is where you will actually see what the network is doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost is per edge and per attachment.&lt;/strong&gt; You pay an hourly rate for each core network edge and each attachment, plus data processing. There is no per-request billing. A dormant region with an edge still costs money, so only list regions you actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth it?
&lt;/h2&gt;

&lt;p&gt;For a multi-region, multi-account network, yes. You replace a mesh of regional Transit Gateways and their route tables with one policy. You halve firewall traversals for inter-region traffic. Attaching a VPC becomes a tag. And "what can test reach?" is answered by a file instead of a scavenger hunt through four regions.&lt;/p&gt;

&lt;p&gt;Cloud WAN is still young and it shows in change velocity and tooling. But the model is right. Describe the WAN, and let AWS run it.&lt;/p&gt;

&lt;p&gt;If you are running a Transit Gateway mesh and weighing this up, I am happy to compare notes. Find me on &lt;a href="https://blog.kasun.dev" rel="noopener noreferrer"&gt;blog.kasun.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>network</category>
    </item>
    <item>
      <title>Cómo darle memoria semántica a un agente de IA sin base de datos vectorial</title>
      <dc:creator>Kevin Lupera</dc:creator>
      <pubDate>Thu, 24 Sep 2026 03:05:33 +0000</pubDate>
      <link>https://dev.to/aws-builders/como-darle-memoria-semantica-a-un-agente-de-ia-sin-base-de-datos-vectorial-505b</link>
      <guid>https://dev.to/aws-builders/como-darle-memoria-semantica-a-un-agente-de-ia-sin-base-de-datos-vectorial-505b</guid>
      <description>&lt;p&gt;Puedes darle a un agente de IA serverless una memoria durable y basada en significado usando &lt;strong&gt;búsqueda vectorial nativa de DynamoDB&lt;/strong&gt; y &lt;strong&gt;embeddings de Amazon Bedrock&lt;/strong&gt;, sin una base de datos vectorial dedicada y sin pipeline de sincronización. En esta demo el embedding vive junto al ítem, un solo &lt;code&gt;PutItem&lt;/code&gt; escribe ambos, y buscar por significado es una única llamada a &lt;code&gt;SearchVectors&lt;/code&gt;. Medido cara a cara en &lt;code&gt;us-east-1&lt;/code&gt;, esta ruta nativa fue &lt;strong&gt;~2.5x más rápida en escrituras&lt;/strong&gt; y &lt;strong&gt;~1.4x más rápida en búsquedas&lt;/strong&gt; (p50) que S3 Vectors para la memoria en la ruta caliente de un agente.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📦 Clona y ⭐ &lt;a href="https://github.com/kevinlupera/strands-dynamo-vectors" rel="noopener noreferrer"&gt;strands-dynamo-vectors&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Este es el fallo del que trata todo el post. A un agente recién creado le preguntas por un dato que le contaron a un agente &lt;em&gt;anterior&lt;/em&gt;. Nunca vio esa conversación, así que no tiene con qué responder — echa mano de una tool, no encuentra nada, y termina pidiéndote que se lo repitas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[session 2] user: What's my production cluster called and its region?

Tool #1: retrieve_context
Could you please provide the reference or context key that holds the production
cluster details? Alternatively, I can try to retrieve this information from other
available sources if you direct me.
&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%2F86doxmpe1n53avzdz1a8.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%2F86doxmpe1n53avzdz1a8.png" alt="Takeaway: un agente nuevo no tiene memoria del turno anterior — tiene que pedirte que se lo repitas" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Esa es una ejecución real de &lt;code&gt;demo.py&lt;/code&gt;, no un experimento mental. Esto golpea a &lt;strong&gt;cualquiera que corra agentes en AWS Lambda&lt;/strong&gt;, donde el contenedor muere entre invocaciones y cada turno es, en la práctica, un agente nuevo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que vas a aprender:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Las tres capas de memoria&lt;/strong&gt;: por qué state, session y memoria a largo plazo no son lo mismo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cómo construir memoria semántica&lt;/strong&gt;: enchufar la búsqueda vectorial nativa de DynamoDB al Strands Harness como backend de memoria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El trade-off&lt;/strong&gt;: latencia y costo medidos de DynamoDB Vectors vs un store dedicado (S3 Vectors).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ¿Por qué un agente "olvida" entre turnos?
&lt;/h2&gt;

&lt;p&gt;Piensa en tu agente como un &lt;strong&gt;asistente personal&lt;/strong&gt; al que reemplazan por otra persona distinta cada vez que le hablas. No es una metáfora de Lambda: es literalmente lo que pasa. El contenedor que tenía la última conversación desapareció, así que entra un asistente nuevo sin cuaderno y sin memoria.&lt;/p&gt;

&lt;p&gt;Para arreglar el "olvido" primero tienes que darte cuenta de que son &lt;strong&gt;tres problemas distintos&lt;/strong&gt;, cada uno con su propio ciclo de vida. Mezclarlos es el bug que hace que un agente "recuerde lo que no debe y olvide lo que sí".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capa&lt;/th&gt;
&lt;th&gt;Qué es&lt;/th&gt;
&lt;th&gt;Duración&lt;/th&gt;
&lt;th&gt;¿Va al modelo?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;key/value que usan tu app y tus tools&lt;/td&gt;
&lt;td&gt;entre requests&lt;/td&gt;
&lt;td&gt;No — inyéctalo tú si hace falta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;el historial de una conversación&lt;/td&gt;
&lt;td&gt;hasta que dejes de reanudar ese id&lt;/td&gt;
&lt;td&gt;Sí — &lt;em&gt;es&lt;/em&gt; el contexto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memoria a largo plazo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;datos durables recuperados por &lt;strong&gt;significado&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;entre ejecuciones sin relación, para siempre&lt;/td&gt;
&lt;td&gt;Se inyecta antes de cada turno&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En términos del asistente: &lt;strong&gt;state&lt;/strong&gt; es la nota adhesiva sobre el escritorio, &lt;strong&gt;session&lt;/strong&gt; es el cuaderno de la reunión de hoy, y la &lt;strong&gt;memoria a largo plazo&lt;/strong&gt; es que el asistente de verdad &lt;em&gt;recuerde&lt;/em&gt; que prefieres desplegar por las mañanas, incluso semanas después y aunque lo digas con otras palabras.&lt;/p&gt;

&lt;p&gt;El Strands Harness (su constructor de agentes ya ensamblado, &lt;code&gt;create_harness&lt;/code&gt;) expone exactamente dos interruptores para esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;       &lt;span class="c1"&gt;# reanudar ESTA conversación
&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;  &lt;span class="c1"&gt;# llevar datos entre TODAS las ejecuciones
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fíjate en que son &lt;strong&gt;ortogonales&lt;/strong&gt;. Session sobrevive al teardown de Lambda; la memoria sobrevive a &lt;em&gt;todo&lt;/em&gt;, incluidas conversaciones que no comparten ninguna palabra.&lt;/p&gt;

&lt;p&gt;Todo el punto de la búsqueda vectorial de DynamoDB es que el vector vive &lt;strong&gt;junto&lt;/strong&gt; al ítem. Una sola tabla es a la vez tu store operacional y tu índice vectorial, sin un segundo servicio.&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%2F91nnfty1okxo8p3n2t41.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%2F91nnfty1okxo8p3n2t41.png" alt="Takeaway: el embedding vive en la misma tabla de DynamoDB — sin pipeline de sincronización, una sola escritura" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contrasta las dos formas. &lt;strong&gt;RAG&lt;/strong&gt; significa Retrieval-Augmented Generation: darle al modelo datos relevantes antes de que responda.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Enfoque&lt;/th&gt;
&lt;th&gt;Piezas&lt;/th&gt;
&lt;th&gt;Pipeline de sincronización&lt;/th&gt;
&lt;th&gt;Lecturas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vector DB dedicada&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DynamoDB + Streams + Lambda worker + OpenSearch/Pinecone&lt;/td&gt;
&lt;td&gt;Sí — Streams → Lambda → índice externo&lt;/td&gt;
&lt;td&gt;latencia por consistencia eventual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DynamoDB Vectors (esta demo)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solo DynamoDB (ítem + vector)&lt;/td&gt;
&lt;td&gt;Ninguno&lt;/td&gt;
&lt;td&gt;milisegundos de un dígito en la región&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Paso 1: Crear la tabla con un índice vectorial nativo
&lt;/h2&gt;

&lt;p&gt;Declara el índice vectorial &lt;strong&gt;al crear la tabla&lt;/strong&gt;, no después. Agregarlo a una tabla que ya tiene ítems dispara un backfill que pagas en tiempo de reloj. Esta es la declaración del índice de &lt;code&gt;setup_table.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;KeySchema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KeyType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HASH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KeyType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RANGE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;BillingMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# on-demand es OBLIGATORIO para índices vectoriales
&lt;/span&gt;    &lt;span class="n"&gt;VectorIndexes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IndexName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INDEX_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VectorAttribute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VECTOR_ATTRIBUTE&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SearchSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SearchSchemaElementType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HASH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Projection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ProjectionType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EMBED_DIMS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# DEBE coincidir con el modelo de embeddings (1024)
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DistanceFunction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DISTANCE_FUNCTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# COSINE
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué importa:&lt;/strong&gt; dos campos deben coincidir o la búsqueda devuelve silenciosamente nada útil — &lt;code&gt;Dimensions&lt;/code&gt; debe igualar la salida de tu modelo de embeddings (Titan V2 = 1024), y &lt;code&gt;DistanceFunction&lt;/code&gt; debe coincidir con cómo generaste los vectores (&lt;code&gt;COSINE&lt;/code&gt; para embeddings de longitud unitaria).&lt;/p&gt;

&lt;p&gt;Ejecútalo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python setup_table.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[setup] region=us-east-1 table=strands-agent-memory
[setup] create-table sent for 'strands-agent-memory' with vector index 'memory-vector-idx' (1024 dims, COSINE)
[setup] waiting for table to become ACTIVE ...
[setup]   table=ACTIVE index=ACTIVE backfilling=None
[setup] confirming the search endpoint serves the index ...
[setup]   searchable on attempt 1
[setup] DONE. The table is ready for writes and semantic search.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué importa:&lt;/strong&gt; que &lt;code&gt;DescribeTable&lt;/code&gt; reporte &lt;code&gt;ACTIVE&lt;/code&gt; es necesario pero &lt;strong&gt;no suficiente&lt;/strong&gt; — el endpoint de búsqueda puede ir con retraso. El script demuestra que está listo lanzando una llamada real a &lt;code&gt;SearchVectors&lt;/code&gt; en un bucle de reintentos, así que cuando imprime &lt;code&gt;searchable on attempt N&lt;/code&gt;, el índice de verdad sirve consultas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 2: Convertir texto en vector con Titan
&lt;/h2&gt;

&lt;p&gt;DynamoDB almacena y busca vectores, pero &lt;strong&gt;no&lt;/strong&gt; los genera. Tu código llama a Bedrock. Este es el helper completo de embeddings de &lt;code&gt;embeddings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Devuelve el embedding de Titan V2 para text como lista de floats.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# amazon.titan-embed-text-v2:0
&lt;/span&gt;        &lt;span class="n"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EMBED_DIMS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normalize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué importa:&lt;/strong&gt; &lt;code&gt;normalize=True&lt;/code&gt; produce vectores de longitud unitaria, que es lo que espera la búsqueda &lt;code&gt;COSINE&lt;/code&gt;. Y como es &lt;em&gt;tu&lt;/em&gt; proceso el que llama a Bedrock, cada escritura semántica y cada búsqueda son &lt;strong&gt;dos llamadas facturables&lt;/strong&gt; — una a Titan, una a DynamoDB — que es también por qué el rol de IAM necesita &lt;code&gt;bedrock:InvokeModel&lt;/code&gt;, no solo permisos de DynamoDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 3: Implementar el store de memoria que el Harness entiende
&lt;/h2&gt;

&lt;p&gt;El Harness trae un file store cuya búsqueda es solapamiento de tokens por palabra clave: matchea &lt;strong&gt;palabras&lt;/strong&gt;. Nosotros queremos que matchee &lt;strong&gt;significado&lt;/strong&gt;. El store implementa el protocolo &lt;code&gt;strands.memory.MemoryStore&lt;/code&gt;; los dos métodos que importan son &lt;code&gt;add()&lt;/code&gt; y &lt;code&gt;search()&lt;/code&gt;, de &lt;code&gt;ddb_memory_store.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Genera el embedding del dato y lo guarda como vector buscable en DynamoDB.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memories/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# primero el embedding, luego una escritura guarda ambos
&lt;/span&gt;        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryEntry&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Genera el embedding de la query y devuelve las memorias más similares por significado.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;SearchQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_search_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# acota la búsqueda a un solo tenant
&lt;/span&gt;            &lt;span class="n"&gt;include_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... mapea los resultados a MemoryEntry(content=..., metadata={"score": r.score})
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué importa:&lt;/strong&gt; son &lt;code&gt;async&lt;/code&gt;, tienes que hacerles &lt;code&gt;await&lt;/code&gt;. Un &lt;code&gt;write&lt;/code&gt; sin &lt;code&gt;await&lt;/code&gt; no hace nada silenciosamente y tu tabla queda vacía. Ese &lt;code&gt;await&lt;/code&gt; que falta es la trampa que más tiempo me costó (más abajo).&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 4: Demostrar el recall por significado y luego conectarlo al agente
&lt;/h2&gt;

&lt;p&gt;Guarda un dato en una "conversación" y luego haz una pregunta &lt;strong&gt;sin relación&lt;/strong&gt; que no comparta ninguna palabra de contenido. De &lt;code&gt;demo.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The user prefers deployments to happen on Tuesday mornings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# deja que el índice vectorial se asiente (consistencia eventual)
&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;when does this user like to ship releases?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[write] stored in some conversation: 'The user prefers deployments to happen on Tuesday mornings.'
[search] asked in ANOTHER conversation, by meaning: 'when does this user like to ship releases?'
  score=0.6665  The user prefers deployments to happen on Tuesday mornings.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué importa:&lt;/strong&gt; la query dice "ship releases", el dato dice "deployments"; la query no dice nada de "Tuesday". Un &lt;code&gt;Query&lt;/code&gt; por prefijo nunca encuentra esto. El índice vectorial sí, porque busca significado, no texto — el score de coseno quedó en &lt;strong&gt;0.6665&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ahora enchufa ese mismísimo store a un agente Harness completo como su backend de memoria — esta es la juntura documentada (el &lt;em&gt;seam&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBVectorMemoryStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DEMO_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;INDEX_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_search_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;-- el seam: el Harness posee la inyección + la tool search_memory
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m planning next week&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s release. Is there any timing preference on file for me?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[agent] user: I'm planning next week's release. Any timing preference on file?
[agent] agent: You've indicated a preference for deployments to happen on Tuesday
mornings. This is noted for planning next week's release.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué importa:&lt;/strong&gt; el Harness sigue siendo dueño del manager de memoria. Llamó a &lt;code&gt;search_memory&lt;/code&gt;, sacó la preferencia de DynamoDB y la inyectó en el contexto &lt;em&gt;antes&lt;/em&gt; de que el modelo respondiera. Misma tabla, mismo índice — ahora moviendo a un agente real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que me costó tiempo
&lt;/h2&gt;

&lt;p&gt;Los métodos &lt;code&gt;add()&lt;/code&gt;/&lt;code&gt;search()&lt;/code&gt; son &lt;code&gt;async&lt;/code&gt;, y &lt;code&gt;strands-dynamodb-storage&lt;/code&gt; (v0.1.x) es joven. Escribí un store, corrí la demo y obtuve una &lt;strong&gt;tabla vacía sin ningún error&lt;/strong&gt;. Una escritura sin &lt;code&gt;await&lt;/code&gt; devuelve una corrutina y no hace nada silenciosamente — sin excepción, sin fila.&lt;/p&gt;

&lt;p&gt;💡 Si tu búsqueda semántica no devuelve nada y no hay error, revisa que cada &lt;code&gt;await self._storage.write(...)&lt;/code&gt; y &lt;code&gt;await store.add(...)&lt;/code&gt; esté realmente con &lt;code&gt;await&lt;/code&gt;. Un &lt;code&gt;await&lt;/code&gt; que falta es un no-op silencioso, no un crash.&lt;/p&gt;

&lt;p&gt;Lo segundo: que &lt;code&gt;DescribeTable&lt;/code&gt; diga &lt;code&gt;ACTIVE&lt;/code&gt; no significa que el índice ya vaya a responder una llamada a &lt;code&gt;SearchVectors&lt;/code&gt;. Haz polling con una búsqueda real (como hace &lt;code&gt;setup_table.py&lt;/code&gt;) en lugar de confiar en el estado de la tabla.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuánto cuesta y es más rápido?
&lt;/h2&gt;

&lt;p&gt;Los mismos 30 embeddings de Titan V2 (1024 dims) escritos en cada store, luego 30 búsquedas (TopK=5) en cada uno, midiendo solo alrededor de la llamada al store. Un solo escritor, ejecutado &lt;strong&gt;desde fuera de AWS&lt;/strong&gt;, así que los números incluyen el round trip cliente↔región; dentro de una VPC son más bajos. Los datos crudos están en &lt;code&gt;metrics.json&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;Store&lt;/th&gt;
&lt;th&gt;write p50&lt;/th&gt;
&lt;th&gt;write p95&lt;/th&gt;
&lt;th&gt;search p50&lt;/th&gt;
&lt;th&gt;search p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DynamoDB Vectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;121.05 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;130.24 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;122.09 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;140.87 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3 Vectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;304.36 ms&lt;/td&gt;
&lt;td&gt;401.06 ms&lt;/td&gt;
&lt;td&gt;167.15 ms&lt;/td&gt;
&lt;td&gt;257.27 ms&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%2F3ypu19qhqfic8t0wddp2.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%2F3ypu19qhqfic8t0wddp2.png" alt="Takeaway: DynamoDB Vectors lidera en todas las métricas de latencia para memoria en ruta caliente" width="799" height="444"&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%2F7ftshm0s17m809yrfdbk.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%2F7ftshm0s17m809yrfdbk.png" alt="Takeaway: la latencia de búsqueda de DynamoDB es más ajustada y más baja por request" width="799" height="444"&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%2Frmf6ytkzp2ryu82kfbms.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%2Frmf6ytkzp2ryu82kfbms.png" alt="Takeaway: ~2.5x más rápido en escrituras, ~1.4x más rápido en búsquedas en p50" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sobre el costo: los vectores viven en el almacenamiento normal de DynamoDB — los primeros 25 GB-mes son gratis, luego ~$0.25/GB-mes; el equivalente a una demo de memorias son kilobytes. Las escrituras y búsquedas se facturan por GB (la búsqueda tiene un mínimo de 1 KB), y el artículo de referencia midió &lt;strong&gt;US$0.0004&lt;/strong&gt; por 100 memorias + 30 búsquedas. Recuerda: cada operación semántica son dos llamadas facturables (Titan + DynamoDB).&lt;/p&gt;

&lt;p&gt;Medido en una ejecución de un solo escritor, sin concurrencia, desde fuera de AWS, septiembre de 2026. Tómalos como orden de magnitud, no como un SLA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo usarlo (y cuándo no)
&lt;/h2&gt;

&lt;p&gt;No es "cuál es mejor", es &lt;strong&gt;qué representan tus vectores&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Memoria de agente, usuarios, productos, recomendaciones, señales de fraude&lt;/strong&gt;: datos operacionales que mutan seguido y se leen en la ruta caliente. DynamoDB Vectors — una escritura, cero pipeline de sincronización, lecturas de milisegundos de un dígito en la región.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;PDFs, docs, wikis, corpus de RAG&lt;/strong&gt;: embed una vez, consulta muchas, enormes y mayormente inactivos. Usa &lt;strong&gt;S3 Vectors&lt;/strong&gt; — optimizado para el menor costo de almacenamiento (~90% más barato para ese patrón).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ¿Cómo hago la limpieza?
&lt;/h2&gt;

&lt;p&gt;Con PAY_PER_REQUEST no hay cargo por capacidad inactiva, pero el almacenamiento factura por GB-mes mientras la tabla exista. Borra todo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python cleanup.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto borra la tabla de DynamoDB (su índice vectorial se va con ella), el bucket y el índice de S3 Vectors creados por el benchmark, y los archivos locales de sesión bajo &lt;code&gt;./.agent&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Necesito una base de datos vectorial aparte para la memoria del agente?&lt;/strong&gt; No. Si los vectores son operacionales — creados y leídos en la ruta caliente, como la memoria de un agente — la búsqueda vectorial nativa de DynamoDB los mantiene junto al ítem, así que no hay un segundo servicio ni pipeline de sincronización.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿La partition key es un límite de seguridad?&lt;/strong&gt; No. El &lt;code&gt;pk&lt;/code&gt; acota y acelera una búsqueda, pero cualquiera con &lt;code&gt;dynamodb:SearchVectors&lt;/code&gt; sobre el índice puede consultar cualquier partición. El aislamiento real entre tenants vive en IAM — tablas o índices separados si necesitas aislamiento fuerte.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué versión mínima del SDK necesito?&lt;/strong&gt; boto3/botocore ≥ 1.43.64. &lt;code&gt;SearchVectors&lt;/code&gt; de DynamoDB llegó al modelo de servicio del AWS SDK el 2026-08-04; los SDKs anteriores no exponen &lt;code&gt;client.search_vectors&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Puntos clave
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;La memoria son tres cosas, no una.&lt;/strong&gt; State, session y memoria a largo plazo tienen duraciones distintas; la mayoría de las necesidades de "memoria" eran solo una session sobreviviendo al teardown de Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los vectores operacionales viven junto a su ítem.&lt;/strong&gt; La búsqueda vectorial nativa de DynamoDB elimina la vector DB dedicada y su pipeline de sincronización para la memoria del agente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ajusta el store al perfil del dato.&lt;/strong&gt; DynamoDB Vectors para datos calientes que mutan; S3 Vectors para bases de conocimiento grandes y mayormente inactivas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tu agente ya no tiene que ser el asistente al que reemplazan cada mañana y que tiene que pedirte que le repitas todo: puede recordar de verdad lo que le contaste, incluso semanas después, incluso con otras palabras.&lt;/p&gt;

&lt;p&gt;¿Qué necesita recordar &lt;em&gt;tu&lt;/em&gt; agente entre conversaciones, y lo estás guardando como state, session o memoria semántica? Cuéntame en los comentarios.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html" rel="noopener noreferrer"&gt;Amazon Bedrock — Titan Text Embeddings V2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" rel="noopener noreferrer"&gt;Amazon DynamoDB Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;¡Gracias!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>python</category>
      <category>agents</category>
    </item>
    <item>
      <title>Plain Gemma 4 26B vs Jev on One EC2 L4: 2.1 Points Behind Overall, Level on Yes/No, 4.5 Behind on Multiple Choice</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 24 Sep 2026 02:00:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-3ao6</link>
      <guid>https://dev.to/aws-builders/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-3ao6</guid>
      <description>&lt;p&gt;This article provides a step by step guide to measuring Gemma 4 26B as a Jev-style decision model on an AWS EC2 L4 GPU, and compares a plain read of its label probabilities against DiffusionGemma's one-step read and against Jev's published results. The measurement was pre-registered, and every per-item output is committed.&lt;/p&gt;

&lt;p&gt;On Bespoke Labs' 3,880-record public suite, plain Gemma 4 26B trails Jev 1.13.0 by 2.1 points overall, shows no measurable difference on yes/no questions, and trails by 4.5 points on multiple choice. Jev is better calibrated out of the box, and one temperature fitted on 50 labels brings Gemma's median calibration error within 0.01 of Jev's. Against DiffusionGemma, the plain read is level on accuracy, worse calibrated before fitting and similar after, and 1.9 to 5.1 times faster per decision.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  Why Measure This?
&lt;/h4&gt;

&lt;p&gt;A Jev-style decision model answers a typed question with a probability for each allowed option, in one forward pass, with no generated text. TypeSafe's Jev does this as a hosted service. Any open model can do it: end the prompt where the answer starts, read the scores of the allowed label tokens, and apply a softmax over those.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln"&gt;companion review of the independent evidence on Jev&lt;/a&gt; found two open questions for Gemma. Plain Gemma read this way had no published accuracy or calibration result. DiffusionGemma, read through vLLM PR #57250, had been described by Google's Gemma account on September 18 as "yielding well-calibrated decision distributions", with no published measurement behind it.&lt;/p&gt;

&lt;p&gt;This run answers both on the same GPU, the same prompts, the same label tokens and the same scoring code, using community 4-bit (AWQ) builds of both models. A third run puts both, and Gemma 4 E4B, on the public suite where Bespoke Labs has published results for Jev, so the Gemma numbers sit beside Jev's on identical records.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;An AWS account with quota for one G-family instance, and the AWS CLI logged in&lt;/li&gt;
&lt;li&gt;A security group, subnet and instance profile with &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; for remote commands&lt;/li&gt;
&lt;li&gt;Python 3 with &lt;code&gt;transformers&lt;/code&gt; and &lt;code&gt;pybase64&lt;/code&gt; for the client side&lt;/li&gt;
&lt;li&gt;The repository cloned: &lt;code&gt;git clone https://github.com/xbill9/gemma4-dev&lt;/code&gt; and &lt;code&gt;cd gemma4-dev/jev&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Pre-Register the Measurement
&lt;/h4&gt;

&lt;p&gt;The models, image, serving flags, data, metrics and comparisons are written down and committed before any model call. The file is &lt;code&gt;PREREGISTRATION.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; 3c67f62 d94471d a5cda09 894323e adf0c16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3c67f62 jev: pre-registration for the L4 AWQ run; option-order variant, label-count calibration curve, EC2 user-data
d94471d jev: run 2026-09-23-l4-awq — Gemma 4 26B label logits vs DiffusionGemma one-step reads, both AWQ 4-bit on one EC2 L4; per-item outputs, summary, host evidence
a5cda09 jev: pre-registration addendum (latency pass on-instance, off-label tokens, E2B/E4B bf16 arms); 20-split calibration analysis; archived run log and throughput arithmetic
894323e jev: plain arm builds its prompt from the served model's own chat template (26B unchanged, checked); E4B/E2B deviation recorded
adf0c16 jev: Bespoke Labs public-suite run — rebuild with checksum checks, runner via the proxy's Jev parser, scorer matched to Nimble's definitions, instance driver, generic launcher; pre-registration addendum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latency and small-model runs were added in one pre-registration addendum after the first run, and the public-suite run in another; each was committed before its own first call, and each deviation from the plan is recorded there. The pre-registration commits to publishing every result, including any where either arm does worse.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Pick a Matched Pair of Checkpoints
&lt;/h4&gt;

&lt;p&gt;An NVIDIA L4 has 24 GB of memory, so both 26B models run at 4 bits. Both checkpoints come from the same uploader and apply the same quantization settings to both models, so what differs is the model and how it is read.&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;Plain arm&lt;/th&gt;
&lt;th&gt;Diffusion arm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cyankiwi/diffusiongemma-26B-A4B-it-AWQ-INT4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;compressed-tensors, 4-bit, group 32, symmetric&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;17.19 GB&lt;/td&gt;
&lt;td&gt;17.22 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both arms share one set of serving flags.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Launch One L4 on EC2
&lt;/h4&gt;

&lt;p&gt;The security group opens port 8000 to one address only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-security-group &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--group-name&lt;/span&gt; jev-eval-sg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"jev eval: vLLM 8000 from one IP"&lt;/span&gt; &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; &amp;lt;vpc-id&amp;gt; &lt;span class="nt"&gt;--query&lt;/span&gt; GroupId &lt;span class="nt"&gt;--output&lt;/span&gt; text
aws ec2 authorize-security-group-ingress &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--group-id&lt;/span&gt; &amp;lt;sg-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="nt"&gt;--cidr&lt;/span&gt; &amp;lt;your-ip&amp;gt;/32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance boots the regional GPU Deep Learning AMI and runs &lt;code&gt;aws/user-data.sh&lt;/code&gt;, which pulls a vLLM nightly that contains PR #57250 and serves one model at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 run-instances &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--image-id&lt;/span&gt; &amp;lt;gpu-dlami&amp;gt; &lt;span class="nt"&gt;--instance-type&lt;/span&gt; g6.xlarge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &amp;lt;subnet-id&amp;gt; &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &amp;lt;sg-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-instance-profile&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;profile&amp;gt; &lt;span class="nt"&gt;--user-data&lt;/span&gt; file://aws/user-data.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--block-device-mappings&lt;/span&gt; &lt;span class="s1"&gt;'DeviceName=/dev/sda1,Ebs={VolumeSize=100,VolumeType=gp3,DeleteOnTermination=true}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=jev-eval},{Key=ManagedBy,Value=jev}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;i-0c5b14e913b4c1019 g6.xlarge   us-east-1a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;g6.xlarge&lt;/code&gt; has the same L4 as the larger sizes with 16 GB of host memory, so &lt;code&gt;user-data.sh&lt;/code&gt; adds a 16 GB swapfile on hosts under 30 GB; both 17 GB checkpoints loaded with it in place.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Serve the Plain Arm
&lt;/h4&gt;

&lt;p&gt;One flag set serves both arms: Triton attention, eager mode, prefix caching, 2048-token context, 16 sequences and 32 logprobs. The diffusion arm adds a 64-token canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/opt/jev/serve.sh ar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client waits on &lt;code&gt;/v1/models&lt;/code&gt; until the model answers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;READY after ~480s
['cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers the image pull, the 17 GB download and the load. The host record from the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NVIDIA L4, 595.91.07, 23034 MiB
image: vllm/vllm-openai:nightly-e9757321527ca1ecd514c07c1418dd2c53da3d19 digest: sha256:f75fec992c293dd41042745f8aa71bfdf12b188b6a01e0baf075e4bca5d297e5
vllm 0.29.1rc1.dev573+ge97573215 torch 2.13.0+cu130
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — Check the Read Before the Full Run
&lt;/h4&gt;

&lt;p&gt;Both arms use the prompt, answer template, label tokens and probability code from the PR's own &lt;code&gt;structured_server.py&lt;/code&gt;. &lt;code&gt;run_eval.py&lt;/code&gt; confirms both chat templates render the identical prefix before it sends anything. A five-example run checks the path end to end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://&amp;lt;host&amp;gt;:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit &lt;span class="nt"&gt;--run&lt;/span&gt; smoke &lt;span class="nt"&gt;--limit&lt;/span&gt; 5 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;autoregressive sst2: 5/5 (5s)
autoregressive ag_news: 5/5 (1s)
autoregressive emotion: 5/5 (0s)
autoregressive irony: 5/5 (0s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record stores the label probabilities, &lt;code&gt;labels_returned&lt;/code&gt;, which confirms every label came back with a real probability, and &lt;code&gt;label_mass&lt;/code&gt;, the share of the model's probability that landed on the allowed labels.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Run Both Arms
&lt;/h4&gt;

&lt;p&gt;The data is 300 labelled examples each from sst2, AG News, DAIR Emotion and tweet_eval irony, stratified with a fixed seed. sst2 and irony carry annotators' labels, AG News its source's news categories, and DAIR Emotion labels taken from the hashtags on each tweet. Each arm runs once as written, and the three choice tasks run once more with their options listed in reverse order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 8
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 8 &lt;span class="nt"&gt;--variant&lt;/span&gt; reversed
/opt/jev/serve.sh diffusion
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; diffusion &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$DIFF&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 4
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; diffusion &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$DIFF&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 4 &lt;span class="nt"&gt;--variant&lt;/span&gt; reversed
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; results/2026-09-23-l4-awq/diffusion-&lt;span class="k"&gt;*&lt;/span&gt;.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    300 results/2026-09-23-l4-awq/diffusion-ag_news.jsonl
    300 results/2026-09-23-l4-awq/diffusion-ag_news--reversed.jsonl
    300 results/2026-09-23-l4-awq/diffusion-emotion.jsonl
    300 results/2026-09-23-l4-awq/diffusion-emotion--reversed.jsonl
    300 results/2026-09-23-l4-awq/diffusion-irony.jsonl
    300 results/2026-09-23-l4-awq/diffusion-sst2.jsonl
    300 results/2026-09-23-l4-awq/diffusion-sst2--reversed.jsonl
   2100 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diffusion arm reads each example four times with different noise in the answer slots, as the PR's proxy does. The DiffusionGemma server came up in about 300 seconds with the image already pulled.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Score It
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 score.py &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;score.py&lt;/code&gt; writes &lt;code&gt;SUMMARY.md&lt;/code&gt; and &lt;code&gt;summary.json&lt;/code&gt; beside the records. Every figure below comes from those two files. Readouts: &lt;code&gt;plain&lt;/code&gt; is one read of plain Gemma. &lt;code&gt;diffusion&lt;/code&gt; is the proxy's automatic readout, the mean of four DiffusionGemma reads on the 96% to 100% of examples where its rule re-reads and one read on the rest; the label-count tables use the four-read mean throughout.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Accurate Is Each One?
&lt;/h4&gt;

&lt;p&gt;Level on three tasks, plain ahead on AG News.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Always-majority&lt;/th&gt;
&lt;th&gt;Plain Gemma&lt;/th&gt;
&lt;th&gt;DiffusionGemma&lt;/th&gt;
&lt;th&gt;Difference (95% range)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;93.3%&lt;/td&gt;
&lt;td&gt;−1.7 (−4.0 to +0.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;🥇 86.7%&lt;/td&gt;
&lt;td&gt;83.0%&lt;/td&gt;
&lt;td&gt;−3.7 (−6.7 to −0.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;35.0%&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;td&gt;+2.0 (−1.0 to +5.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;td&gt;89.7%&lt;/td&gt;
&lt;td&gt;87.3%&lt;/td&gt;
&lt;td&gt;−2.3 (−5.7 to +1.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Throughout, level means the 95% range of the difference includes zero, and 🥇 in an accuracy table marks a lead whose range excludes it; in Compare and Contrast 🥇 marks the better value. Only the AG News difference excludes zero.&lt;/p&gt;




&lt;h4&gt;
  
  
  Are the Raw Probabilities Calibrated?
&lt;/h4&gt;

&lt;p&gt;DiffusionGemma's are closer. Plain Gemma's are strongly overconfident.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain Gemma ECE&lt;/th&gt;
&lt;th&gt;DiffusionGemma ECE&lt;/th&gt;
&lt;th&gt;Difference (95% range)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;0.051&lt;/td&gt;
&lt;td&gt;0.045&lt;/td&gt;
&lt;td&gt;−0.005 (−0.021 to +0.020)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;0.129&lt;/td&gt;
&lt;td&gt;0.119&lt;/td&gt;
&lt;td&gt;−0.010 (−0.030 to +0.020)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;0.386&lt;/td&gt;
&lt;td&gt;0.261&lt;/td&gt;
&lt;td&gt;−0.125 (−0.154 to −0.089)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;0.098&lt;/td&gt;
&lt;td&gt;0.043&lt;/td&gt;
&lt;td&gt;−0.055 (−0.077 to −0.014)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ECE is expected calibration error over 15 bins: the gap between how confident a model says it is and how often it is right. DiffusionGemma is better calibrated on emotion and irony, and level on the other two.&lt;/p&gt;

&lt;p&gt;The fitted temperatures show the size of the gap. On the pre-registered single split, one temperature that best fits the labels was 3.48 to 7.24 for plain Gemma once 25 or more labels were used, and 0.73 to 2.36 for DiffusionGemma. A temperature above 1 means the model's probabilities are too extreme.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Do 50 Labels Buy?
&lt;/h4&gt;

&lt;p&gt;Most of the correction for plain Gemma; a smaller gain for DiffusionGemma, and none on irony.&lt;/p&gt;

&lt;p&gt;Each task's examples split into a fitting half and a held-out half. One temperature is fitted on the first N labels of the fitting half and scored on the held-out half. On the pre-registered single split, 50 labels took plain Gemma from 0.040, 0.139, 0.440 and 0.088 to 0.029, 0.064, 0.102 and 0.042 on sst2, AG News, DAIR Emotion and irony, and DiffusionGemma from 0.047, 0.132, 0.311 and 0.055 to 0.025, 0.061, 0.115 and 0.054.&lt;/p&gt;

&lt;p&gt;One split of 150 held-out examples moves ECE by several hundredths, so the same fit was repeated over 20 random splits, an analysis added after the run. Means, with the lowest and highest split in brackets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain, 0 labels&lt;/th&gt;
&lt;th&gt;Plain, 50 labels&lt;/th&gt;
&lt;th&gt;Diffusion, 0 labels&lt;/th&gt;
&lt;th&gt;Diffusion, 50 labels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;0.048&lt;/td&gt;
&lt;td&gt;0.039 (0.021–0.061)&lt;/td&gt;
&lt;td&gt;0.050&lt;/td&gt;
&lt;td&gt;0.042 (0.025–0.081)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;0.130&lt;/td&gt;
&lt;td&gt;0.066 (0.037–0.132)&lt;/td&gt;
&lt;td&gt;0.121&lt;/td&gt;
&lt;td&gt;0.085 (0.060–0.138)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;0.392&lt;/td&gt;
&lt;td&gt;0.089 (0.047–0.161)&lt;/td&gt;
&lt;td&gt;0.266&lt;/td&gt;
&lt;td&gt;0.111 (0.062–0.155)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;0.099&lt;/td&gt;
&lt;td&gt;0.060 (0.035–0.130)&lt;/td&gt;
&lt;td&gt;0.054&lt;/td&gt;
&lt;td&gt;0.066 (0.031–0.116)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With no labels, DiffusionGemma is better calibrated on emotion and irony. With 50, plain Gemma's mean is at or below DiffusionGemma's on all four tasks, and the split-to-split ranges overlap throughout.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does DiffusionGemma's Read Do?
&lt;/h4&gt;

&lt;p&gt;About half of its probability lands outside the allowed labels. The median share on the allowed labels per task:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain Gemma&lt;/th&gt;
&lt;th&gt;DiffusionGemma&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;td&gt;0.370&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.392&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.593&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At the answer slot the server returns the allowed labels plus the single most likely token. In the latency run that token was &lt;code&gt;&amp;lt;eos&amp;gt;&lt;/code&gt;, the end-of-sequence token, in 40% of reads on sst2, 53% on AG News and 64% on DAIR Emotion, and &lt;code&gt;the&lt;/code&gt; in 24% of reads on irony. The proxy rescales the label probabilities to sum to one, so the answer reads as confident either way; &lt;code&gt;label_mass&lt;/code&gt; is the field that shows it, and it is worth logging in any deployment.&lt;/p&gt;

&lt;p&gt;The proxy re-reads when the answer slot's entropy is above 0.1. It computes that entropy from the returned tokens' full-vocabulary probabilities without rescaling them, so a label holding half the probability on its own already scores 0.35. It re-read 100% of sst2 and AG News examples, 99.0% of DAIR Emotion and 96.3% of irony.&lt;/p&gt;

&lt;p&gt;The re-reads barely change the answer: one read and the mean of four are within 0.6 points of accuracy on every task (93.3% and 93.3% on sst2, 83.3% and 83.0% on AG News, 60.7% and 60.3% on DAIR Emotion, 86.7% and 87.3% on irony). How much the four reads disagree separates right from wrong well on sst2 and irony, AUROC 0.891 and 0.873, and weakly on AG News and DAIR Emotion, 0.685 and 0.635.&lt;/p&gt;




&lt;h4&gt;
  
  
  Does Option Order Change the Answer?
&lt;/h4&gt;

&lt;p&gt;For both models, by similar amounts. Listing the choice options in reverse order changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain Gemma&lt;/th&gt;
&lt;th&gt;DiffusionGemma&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;6 of 300 (2.0%)&lt;/td&gt;
&lt;td&gt;7 of 300 (2.3%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;14 of 300 (4.7%)&lt;/td&gt;
&lt;td&gt;21 of 300 (7.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;27 of 300 (9.0%)&lt;/td&gt;
&lt;td&gt;26 of 300 (8.7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  How Fast Is Each One?
&lt;/h4&gt;

&lt;p&gt;A second run timed both arms with the client on the instance itself, against &lt;code&gt;localhost&lt;/code&gt;, one request at a time, over the first 100 examples of each task. Same image, flags and checkpoints, on a &lt;code&gt;g6.4xlarge&lt;/code&gt;: the same L4 with a larger host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://localhost:8000 &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-latency &lt;span class="nt"&gt;--limit&lt;/span&gt; 100 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 1
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; diffusion &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://localhost:8000 &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$DIFF&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-latency &lt;span class="nt"&gt;--limit&lt;/span&gt; 100 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;--reads&lt;/span&gt; 4 &lt;span class="nt"&gt;--keep-top&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Task | Plain, median | Plain, 90th pct | Diffusion one read, median | Diffusion one read, 90th pct | Diffusion 4 reads (first + 3 parallel), median |
|---|---|---|---|---|---|
| sst2 | 61 | 62 | 118 | 119 | 306 |
| ag_news | 61 | 62 | 119 | 120 | 307 |
| emotion | 61 | 62 | 119 | 120 | 308 |
| irony | 61 | 61 | 121 | 122 | 312 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Times are in milliseconds. A plain Gemma 26B decision takes 61 ms on one L4 for these prompts, which run 105 to 299 tokens with a median of 132; longer inputs such as full tickets or logs take longer. One DiffusionGemma read takes 1.9 to 2.0 times as long, and the automatic rule's four reads 5.0 to 5.1 times, which is the path it takes on nearly every call. At 61 ms one request at a time, one L4 answers about 16 plain decisions a second before any batching.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Fit One Temperature Before You Trust a Threshold
&lt;/h4&gt;

&lt;p&gt;Both models give usable probabilities after one fitted temperature. &lt;code&gt;score.py&lt;/code&gt; does it with a grid search on log loss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fit_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T_GRID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;temper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;gold_idx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Label 50 real decisions from the task, fit on them, and set any act-or-escalate threshold on the rescaled probabilities. Plain Gemma needs the larger correction, and both reach similar calibration once it is applied.&lt;/p&gt;




&lt;h4&gt;
  
  
  What About the Smaller Gemma 4 Models?
&lt;/h4&gt;

&lt;p&gt;The label read works with any Gemma 4. DiffusionGemma ships only at 26B-A4B, so the smaller models get the plain arm alone, run in bf16 on the same L4 with the same flags. Their chat templates end at the model turn without the empty thought block the 26B template adds, so their prompt follows their own template. These two arms were added in a pre-registration addendum after the first run, and are exploratory. The 26B column is 4-bit and the others bf16, so the gaps mix model size with precision. All three read letter labels (A to F) with the meanings listed in the prompt, the format of the PR's proxy; smaller models may do better with the label words as the answers themselves.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;26B, 4-bit&lt;/th&gt;
&lt;th&gt;E4B, bf16&lt;/th&gt;
&lt;th&gt;E2B, bf16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;94.3%&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;td&gt;30.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;54.0%&lt;/td&gt;
&lt;td&gt;53.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;89.7%&lt;/td&gt;
&lt;td&gt;84.3%&lt;/td&gt;
&lt;td&gt;72.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;E4B is 0.7 to 5.4 points behind the 26B, needs a smaller correction (fitted temperature 1.89 to 3.77 at 50 labels, against 4.65 to 7.05, both means over 20 splits), and reaches similar calibration once corrected: 0.042, 0.071, 0.089 and 0.081 against the 26B's 0.039, 0.066, 0.089 and 0.060. It changed more answers when the options were reversed on AG News, 35 of 300 against 14.&lt;/p&gt;

&lt;p&gt;E2B answered "world" on 277 of 300 AG News examples, and on 263 when the options were listed in reverse order, so the same option wins whatever its position, and its topic accuracy sits near the 25% majority rate. Its fitted temperatures reach 7.99, the top of the pre-registered search range, so its corrected calibration figures understate what a wider search would reach.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Does It Compare With Jev?
&lt;/h4&gt;

&lt;p&gt;Bespoke Labs, which makes the open Nimble-9B decision model, ran Jev 1.13.0 through its API on a 13-subset public suite and published the results per subset, with the converters and record ids. The suite holds 3,880 human-labelled records: yes/no questions from BoolQ, PAWS, SQuAD 2.0, Civil Comments and Aegis 2.0; multiple choice from MultiNLI, PubMedQA, VitaminC and MASSIVE intents in English and German; and five-level ratings from HelpSteer2 and SummEval.&lt;/p&gt;

&lt;p&gt;A third run, on a &lt;code&gt;g6.xlarge&lt;/code&gt; with the same image and flags, rebuilt the suite from the public sources and read it with all three Gemma arms. All 13 rebuilt subsets matched the published checksums, so the Gemma figures and Bespoke Labs' Jev figures come from the same records.&lt;/p&gt;

&lt;p&gt;Each record is a Jev request, which the PR's proxy converts with its own Jev parser, so the Gemma arms see the same prompt format as the first run: yes/no questions answered with the word yes or no, multiple-choice options lettered A, B, C with the answer read as a letter, and rating levels numbered from 1. Gemma was read in that format as published, with no prompt tuning, while Jev takes the request in its own format; a different prompt could move the multiple-choice gap in either direction. Scoring uses Bespoke Labs' definitions, and &lt;code&gt;tests/test_suite_scoring.py&lt;/code&gt; checks it against Bespoke Labs' own code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash nimble_suite/build.sh &amp;lt;workdir&amp;gt;
python3 nimble_suite/run_suite.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://localhost:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--records&lt;/span&gt; &amp;lt;workdir&amp;gt;/public &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-24-l4-suite
python3 nimble_suite/suite_stats.py &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-24-l4-suite &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-24-l4-suite-e4b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accuracy pooled over records, with the 95% range in brackets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Questions&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;th&gt;Jev 1.13.0, published&lt;/th&gt;
&lt;th&gt;Nimble-9B, published&lt;/th&gt;
&lt;th&gt;Plain Gemma 26B&lt;/th&gt;
&lt;th&gt;DiffusionGemma 26B&lt;/th&gt;
&lt;th&gt;Gemma 4 E4B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;3,880&lt;/td&gt;
&lt;td&gt;77.3% (76.0–78.6)&lt;/td&gt;
&lt;td&gt;75.9%&lt;/td&gt;
&lt;td&gt;75.3% (73.9–76.6)&lt;/td&gt;
&lt;td&gt;75.9% (74.5–77.2)&lt;/td&gt;
&lt;td&gt;73.3% (71.9–74.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes/no&lt;/td&gt;
&lt;td&gt;1,399&lt;/td&gt;
&lt;td&gt;84.6% (82.6–86.4)&lt;/td&gt;
&lt;td&gt;80.1%&lt;/td&gt;
&lt;td&gt;84.8% (82.8–86.6)&lt;/td&gt;
&lt;td&gt;84.5% (82.5–86.3)&lt;/td&gt;
&lt;td&gt;81.9% (79.8–83.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple choice&lt;/td&gt;
&lt;td&gt;1,848&lt;/td&gt;
&lt;td&gt;82.8% (81.1–84.5)&lt;/td&gt;
&lt;td&gt;81.1%&lt;/td&gt;
&lt;td&gt;78.3% (76.4–80.1)&lt;/td&gt;
&lt;td&gt;77.3% (75.4–79.2)&lt;/td&gt;
&lt;td&gt;74.8% (72.8–76.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Five-level rating&lt;/td&gt;
&lt;td&gt;633&lt;/td&gt;
&lt;td&gt;45.2% (41.3–49.1)&lt;/td&gt;
&lt;td&gt;51.2%&lt;/td&gt;
&lt;td&gt;45.5% (41.7–49.4)&lt;/td&gt;
&lt;td&gt;52.8% (48.9–56.6)&lt;/td&gt;
&lt;td&gt;49.6% (45.7–53.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev leads plain Gemma by 2.1 points over all records (95% range 0.2 to 4.0) and by 4.5 on multiple choice (2.0 to 7.1). Jev's per-record answers are unpublished, so these ranges compare two independent proportions: pairing would narrow them, and records that share a passage or article widen them, most of all for ratings. The overall gap's range starts at 0.2 points, so it is the least secure of the three; the multiple-choice gap is the firm one. PubMedQA and VitaminC each account for 33 of the 84 records behind it, and PubMedQA has the largest single-subset gap, 77.2% against 64.0%.&lt;/p&gt;

&lt;p&gt;On yes/no questions pooled, plain Gemma shows no measurable difference from Jev, 84.8% against 84.6%, a difference anywhere from 2.8 points ahead to 2.5 behind. Within them Jev is ahead on BoolQ by 5.7 points and plain Gemma on Civil Comments and SQuAD 2.0 by 6.0 and 6.4; PAWS, 5.2 points to Jev, is within its range.&lt;/p&gt;

&lt;p&gt;On ratings DiffusionGemma leads both Jev, by 7.6 points (2.1 to 13.1), and plain Gemma, on exact-level accuracy; Bespoke Labs advises reading that beside the error of the probability-weighted level, which this article does not report. Plain Gemma and DiffusionGemma are level over all 3,880 records: 187 right only for plain, 211 right only for DiffusionGemma, exact McNemar p = 0.25. On ratings DiffusionGemma was right alone on 97 records against 51, p = 0.0002, and the whole gap comes from SummEval, whose 384 records come from 24 news articles. Records from one article move together, so that p-value overstates the evidence.&lt;/p&gt;

&lt;p&gt;Bespoke Labs' open Nimble-9B scores 75.9% over all records, level with the two 26B reads. Gemma 4 E4B trails the 26B by 2.0 points over the whole suite, with single subsets ranging from 9.6 points ahead to 8.0 behind, and trails Jev by 4.1 (2.2 to 6.0).&lt;/p&gt;

&lt;p&gt;Median calibration error over the 13 subsets, with Bespoke Labs' 10 bins:&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;ECE as shipped&lt;/th&gt;
&lt;th&gt;ECE after 50 labels&lt;/th&gt;
&lt;th&gt;Brier as shipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jev 1.13.0, published&lt;/td&gt;
&lt;td&gt;0.071&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.267&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nimble-9B, published&lt;/td&gt;
&lt;td&gt;0.109&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.314&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain Gemma 26B&lt;/td&gt;
&lt;td&gt;0.180&lt;/td&gt;
&lt;td&gt;0.080&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DiffusionGemma 26B&lt;/td&gt;
&lt;td&gt;0.114&lt;/td&gt;
&lt;td&gt;0.074&lt;/td&gt;
&lt;td&gt;0.290&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;0.173&lt;/td&gt;
&lt;td&gt;0.077&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As shipped, Jev has a lower calibration error than plain Gemma on all 13 subsets, than DiffusionGemma on 11 and than E4B on 12. Brier score, which rewards accuracy and calibration together, favours Jev too: plain Gemma and E4B beat it on 2 of 13 subsets, DiffusionGemma on 4. DiffusionGemma's raw calibration error is lower than plain Gemma's on all 13, and its median share on the allowed labels was 66.5% per read on this suite.&lt;/p&gt;

&lt;p&gt;After one temperature per subset, fitted on 50 labels from that subset, the Gemma medians sit 0.003 to 0.009 above Jev's as-shipped median. Per subset, fitted plain Gemma is still above Jev on 8 of 13, by up to 0.049, and the Gemma arms are at or below Jev on 4 to 6. The fitted figures are scored on the held-out half of each subset, 72 to 300 records, and ECE reads higher on fewer records, which works against the Gemma columns. Jev's figures are as shipped, and a temperature fitted on Jev's own output could lower them as well.&lt;/p&gt;

&lt;p&gt;Matt Mastracci, who wrote vLLM PR #57250, compared DiffusionGemma with Jev on 201 hand-built items on September 17, and Google's Gemma account shared the thread the next day. DiffusionGemma answered 198 correctly and Jev 191; he called the two "roughly tied" and DiffusionGemma "the winner, I think." Seven of Jev's ten errors fell in one set, 89 words drawn from five sentences, and the items, code and per-item outputs are unpublished. On the public records DiffusionGemma is 1.4 points behind Jev, anywhere from 0.4 ahead to 3.3 behind, so both measurements find the two close overall; by question type they part, level on yes/no, 5.5 points behind on multiple choice (3.0 to 8.1) and 7.6 ahead on ratings. Plain Gemma 26B, read the same way without the diffusion step, lands within 0.6 points of DiffusionGemma over the suite (p = 0.25), so this suite shows no accuracy gain from the diffusion step.&lt;/p&gt;

&lt;p&gt;His timings show the same cost for the re-reads: 1.9 to 4.6 times one read on his eight sets, against 2.6 times here. One read beat Jev's API on seven of his eight sets and the automatic re-reads lost on all eight; his single reads were timed warm and the re-reads cold, which exaggerates that second gap.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Jev 1.13.0, published&lt;/th&gt;
&lt;th&gt;Plain Gemma 4 26B, label read&lt;/th&gt;
&lt;th&gt;DiffusionGemma 26B, one-step read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy, public suite&lt;/td&gt;
&lt;td&gt;🥇 77.3%&lt;/td&gt;
&lt;td&gt;75.3%, 2.1 behind Jev; level with DiffusionGemma&lt;/td&gt;
&lt;td&gt;75.9%, 1.4 behind Jev, a range that includes a tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes/no, public suite&lt;/td&gt;
&lt;td&gt;84.6%&lt;/td&gt;
&lt;td&gt;84.8%&lt;/td&gt;
&lt;td&gt;84.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple choice, public suite&lt;/td&gt;
&lt;td&gt;🥇 82.8%&lt;/td&gt;
&lt;td&gt;78.3%&lt;/td&gt;
&lt;td&gt;77.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Five-level rating, public suite&lt;/td&gt;
&lt;td&gt;45.2%&lt;/td&gt;
&lt;td&gt;45.5%&lt;/td&gt;
&lt;td&gt;🥇 52.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median ECE, public suite, as shipped&lt;/td&gt;
&lt;td&gt;🥇 0.071&lt;/td&gt;
&lt;td&gt;0.180&lt;/td&gt;
&lt;td&gt;0.114, lower than plain on all 13 subsets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median ECE, public suite, after 50 labels&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.080&lt;/td&gt;
&lt;td&gt;0.074&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median Brier, public suite, as shipped&lt;/td&gt;
&lt;td&gt;🥇 0.267&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;td&gt;0.290&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy, four tasks&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥇 level on three, ahead on AG News&lt;/td&gt;
&lt;td&gt;level on three, 3.7 points behind on AG News&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw calibration, four tasks&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;overconfident, fitted temperature 3.5 to 7.2&lt;/td&gt;
&lt;td&gt;🥇 closer, fitted temperature 0.7 to 2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probability on the allowed labels&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥇 about 100%&lt;/td&gt;
&lt;td&gt;37% to 59%, 66.5% on the public suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per decision&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥇 61 ms on the instance&lt;/td&gt;
&lt;td&gt;118 to 121 ms for one read, 306 to 312 ms with the automatic rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Option-order changes, four tasks&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2.0% to 9.0%&lt;/td&gt;
&lt;td&gt;2.3% to 8.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price per million decisions&lt;/td&gt;
&lt;td&gt;$5.54 at 132 input tokens&lt;/td&gt;
&lt;td&gt;at most $5.43 on a &lt;code&gt;g6.xlarge&lt;/code&gt; at full load&lt;/td&gt;
&lt;td&gt;at most $31.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serving&lt;/td&gt;
&lt;td&gt;TypeSafe's hosted API&lt;/td&gt;
&lt;td&gt;any vLLM&lt;/td&gt;
&lt;td&gt;vLLM with PR #57250&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;Between the two Gemma 4 26B reads, for a Jev-style decision service on one L4: plain Gemma read by its label probabilities, plus one temperature fitted on about 50 labels. It is at least as accurate on the four tasks and level with DiffusionGemma over the public suite, faster, and places all of its probability on the answers you allowed.&lt;/p&gt;

&lt;p&gt;DiffusionGemma's lower raw calibration error has a range excluding zero on two of four tasks and holds on all 13 public-suite subsets, and it matters when no labels exist at all. With 50 labels the difference is no longer detectable on the four tasks, and on the suite the two split 8 to 5, a count within chance (sign test p = 0.58). On five-level ratings DiffusionGemma scored higher than both Jev and plain Gemma, on few source articles; a rating task is the one place to try both.&lt;/p&gt;

&lt;p&gt;Against Jev, on the same public records: plain Gemma 4 26B trails Jev by 2.1 points overall, shows no measurable difference on yes/no questions pooled, and trails it by 4.5 points on multiple choice. Its median calibration error comes within 0.01 of Jev's as-shipped median once one temperature per subset is fitted on 50 labels from that subset; per subset it stays higher on 8 of 13. Jev is the better calibrated with no labels at all.&lt;/p&gt;

&lt;p&gt;Where a smaller model has to do, Gemma 4 E4B in bf16 gives up 0.7 to 5.4 points against the 26B on the four tasks and 2.0 points over the public suite. E2B falls apart on four-way topic classification with this prompt.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does a Decision Cost?
&lt;/h4&gt;

&lt;p&gt;The run logs give a throughput. Plain Gemma answered 2,100 decisions in 51 seconds, 41.2 a second, at client concurrency 8. DiffusionGemma, reading each example four times, answered 2,100 in 298 seconds, 7.0 a second, at concurrency 4. The client was on a home connection, so these are lower bounds on what the L4 serves.&lt;/p&gt;

&lt;p&gt;At the &lt;code&gt;g6.xlarge&lt;/code&gt; on-demand price of $0.8048 an hour, that is at most $5.43 per million decisions for plain Gemma and $31.72 for DiffusionGemma with four reads, which ran at half the concurrency, so its figure is the looser bound. TypeSafe prices Jev at $0.042 per million input tokens, which is $5.54 per million decisions at this run's median of 132 input tokens and $12.56 at the longest, 299: about the same as the L4 at full load. The L4 is charged by the hour whether busy or idle, so its per-decision cost holds only at full load. The public suite's longer prompts would raise both the L4's cost and Jev's.&lt;/p&gt;

&lt;p&gt;The first run, from launch to termination in under 1.2 hours, cost at most $0.97 of instance time. The second, on a &lt;code&gt;g6.4xlarge&lt;/code&gt; at $1.3232 an hour for 1.19 hours, cost $1.57. The public-suite run, on a &lt;code&gt;g6.xlarge&lt;/code&gt; for 0.82 hours, cost $0.66, including rebuilding the suite and reading 11,640 records across three models. All three, $3.20 together, exclude the prorated 100 GB volume. All ran on demand; G-family spot capacity in us-east-1 was unavailable at launch time.&lt;/p&gt;




&lt;h4&gt;
  
  
  Teardown
&lt;/h4&gt;

&lt;p&gt;Every command on the instance went through AWS Systems Manager, so no SSH port was ever open, and port 8000 was open to one address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 terminate-instances &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-0c5b14e913b4c1019
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shutting-down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then delete the security group, and confirm that nothing tagged &lt;code&gt;ManagedBy=jev&lt;/code&gt; remains.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to measure Gemma 4 26B as a Jev-style decision model, read two ways, for accuracy and calibration on labelled data, and to set it beside Jev's published results on a public suite. The key to the solution was a matched pair of 4-bit checkpoints on one EC2 L4, identical prompts and label tokens, and a measurement pre-registered before any call. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚠️ On Bespoke Labs' 3,880-record public suite, plain Gemma 4 26B trails Jev 1.13.0 by 2.1 points overall (range 0.2 to 4.0) and 4.5 on multiple choice (2.0 to 7.1)&lt;/li&gt;
&lt;li&gt;🟢 No measurable difference from Jev on yes/no questions pooled, 84.8% against 84.6%&lt;/li&gt;
&lt;li&gt;⚠️ Jev is the best calibrated with no labels: median ECE 0.071 against 0.114 to 0.180 for the Gemma arms&lt;/li&gt;
&lt;li&gt;🟢 One temperature fitted on 50 labels brings the Gemma medians to 0.074 to 0.080; per subset plain Gemma stays above Jev on 8 of 13&lt;/li&gt;
&lt;li&gt;⚠️ DiffusionGemma 1.4 points behind Jev over the suite (0.4 ahead to 3.3 behind): 5.5 behind on multiple choice, 7.6 ahead on ratings; Mastracci's hand-built items had it 198 against 191, both close overall&lt;/li&gt;
&lt;li&gt;🟢 Plain Gemma and DiffusionGemma level on accuracy: plain ahead only on AG News of the four tasks, 75.3% against 75.9% over the suite, p = 0.25&lt;/li&gt;
&lt;li&gt;🟢 DiffusionGemma better calibrated as shipped, on emotion and irony and on all 13 suite subsets; after 50 labels the difference is no longer detectable&lt;/li&gt;
&lt;li&gt;⚠️ DiffusionGemma places only 37% to 59% of its probability on the allowed labels, and the proxy's rescaling hides it&lt;/li&gt;
&lt;li&gt;⚠️ The automatic re-read rule fires on 96% to 100% of examples, costs 2.6 times one read, and moves accuracy by at most 0.6 points&lt;/li&gt;
&lt;li&gt;🟢 61 ms per plain decision on the instance, and at most $5.43 per million at full load, level with Jev's $5.54 at this run's prompt length&lt;/li&gt;
&lt;li&gt;🟢 Gemma 4 E4B in bf16 0.7 to 5.4 points behind the 26B on the four tasks and 2.0 over the suite&lt;/li&gt;
&lt;li&gt;❌ Gemma 4 E2B answered "world" on 277 of 300 AG News examples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one NVIDIA L4 in us-east-1 on three instances, a &lt;code&gt;g6.xlarge&lt;/code&gt; for the accuracy run, a &lt;code&gt;g6.4xlarge&lt;/code&gt; for the latency and small-model run and a &lt;code&gt;g6.xlarge&lt;/code&gt; for the public suite, vLLM &lt;code&gt;0.29.1rc1.dev573+ge97573215&lt;/code&gt;, the 26B models 4-bit from the same uploader with the same quantization settings and E4B and E2B in bf16, 300 examples per task plus the 3,880-record public suite, one run per arm. Google's reference checkpoints are bf16, and 4-bit quantization may affect a diffusion model differently from an autoregressive one, so results at bf16 may differ for either arm. Latency comes from 100 examples per task with the client on the instance; throughput comes from the first run's client on a home connection. The latency, E4B and E2B arms were added in a pre-registration addendum after the first run, and E4B and E2B are exploratory; the 20-split calibration and the public suite's ranges, paired test, medians and per-subset counts were added after their runs, and the per-subset and pooled suite figures are the pre-registered ones. The suite's E4B arm ran under its own run name, &lt;code&gt;2026-09-24-l4-suite-e4b&lt;/code&gt;, with the same instance, image, flags and records. All four test sets and all 13 suite datasets were published before Gemma 4 and may be in its training data, and the high irony scores may indicate it there; whether they are in Jev's is unknown. No Jev call was made: the Jev and Nimble-9B figures are Bespoke Labs' published results on the same records, from one run of Jev 1.13.0 by a company that publishes a competing model, counting an invalid Jev response as wrong, with Jev's probabilities rounded to two decimals by its API. Code, pre-registration and every per-item output are in the repository. Parts of the analysis and writing were done with AI assistance (Claude); every figure comes from the committed output files.&lt;/p&gt;

&lt;p&gt;The strategy for using label probabilities to run Gemma 4 as a decision model was validated with an incremental step by step approach.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Code, pre-registration and per-item results: &lt;a href="https://github.com/xbill9/gemma4-dev/tree/main/jev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/tree/main/jev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion review of the independent evidence on Jev: &lt;a href="https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln"&gt;https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM PR #57250, DiffusionGemma structured reads: &lt;a href="https://github.com/vllm-project/vllm/pull/57250" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/pull/57250&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Plain checkpoint: &lt;a href="https://huggingface.co/cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit" rel="noopener noreferrer"&gt;https://huggingface.co/cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Diffusion checkpoint: &lt;a href="https://huggingface.co/cyankiwi/diffusiongemma-26B-A4B-it-AWQ-INT4" rel="noopener noreferrer"&gt;https://huggingface.co/cyankiwi/diffusiongemma-26B-A4B-it-AWQ-INT4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DiffusionGemma model card: &lt;a href="https://huggingface.co/google/diffusiongemma-26B-A4B-it" rel="noopener noreferrer"&gt;https://huggingface.co/google/diffusiongemma-26B-A4B-it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google's Gemma account on DiffusionGemma's calibration: &lt;a href="https://x.com/googlegemma/status/2101069861598482817" rel="noopener noreferrer"&gt;https://x.com/googlegemma/status/2101069861598482817&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Matt Mastracci, Jev against DiffusionGemma on hand-built items: &lt;a href="https://x.com/mmastrac/status/2100626193943052784" rel="noopener noreferrer"&gt;https://x.com/mmastrac/status/2100626193943052784&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Guo et al., On Calibration of Modern Neural Networks: &lt;a href="https://arxiv.org/abs/1706.04599" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.04599&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon EC2 G6 instances: &lt;a href="https://aws.amazon.com/ec2/instance-types/g6/" rel="noopener noreferrer"&gt;https://aws.amazon.com/ec2/instance-types/g6/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe Jev: &lt;a href="https://docs.typesafe.ai/concepts/system-one" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/concepts/system-one&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bespoke Labs, Nimble public-suite results for Jev 1.13.0 and Nimble-9B: &lt;a href="https://github.com/bespokelabsai/nimble/blob/0e67403/docs/PUBLIC_BENCHMARKS.md" rel="noopener noreferrer"&gt;https://github.com/bespokelabsai/nimble/blob/0e67403/docs/PUBLIC_BENCHMARKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>aws</category>
      <category>machinelearning</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
