<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[AWS Fullstack - Medium]]></title>
        <description><![CDATA[Providing Best AWS Fullstack Learning Experience - Medium]]></description>
        <link>https://medium.com/awsfullstack?source=rss----a02b1eedcccf---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>AWS Fullstack - Medium</title>
            <link>https://medium.com/awsfullstack?source=rss----a02b1eedcccf---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 22:37:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/awsfullstack" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Building a Hybrid Search Assistant for Product Manuals with Amazon Bedrock and AgentCore]]></title>
            <link>https://medium.com/awsfullstack/building-a-hybrid-search-assistant-for-product-manuals-with-amazon-bedrock-and-agentcore-658357fe808a?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/658357fe808a</guid>
            <category><![CDATA[rags]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[amazon-bedrock]]></category>
            <category><![CDATA[vector-search]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Fri, 13 Nov 2026 12:01:02 GMT</pubDate>
            <atom:updated>2026-11-13T12:01:02.064Z</atom:updated>
            <content:encoded><![CDATA[<h4>Combining Amazon Titan Text Embeddings, Amazon OpenSearch Serverless, Claude on Amazon Bedrock, and AgentCore to answer product questions with grounded sources.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a5sPw_9LUkkrtE0Tt5JTkw.png" /></figure><p>Most product-manual search experiences are built around keyword matching. That works when a customer searches for an exact phrase, model number, or error code. It becomes less reliable when the customer describes a problem in natural language.</p><p>For example, a customer may search for:</p><blockquote><em>Why does my device stop charging after the battery reaches 80%?</em></blockquote><p>The manual might describe the same behavior using completely different wording, such as “battery protection mode” or “charge-limit configuration.” A keyword-only search can miss that connection.</p><p>I built a hybrid search assistant for product manuals to address this problem. The system combines lexical search with semantic vector search, then uses Claude on Amazon Bedrock to generate a grounded answer from the retrieved manual sections.</p><p>This article explains the architecture, the retrieval approach, and how I used AgentCore components to make the workflow more extensible.</p><h3>The Tech Stack</h3><p>The project used the following AWS services and components:</p><ul><li><strong>Embeddings:</strong> Amazon Titan Text Embeddings, to convert manual sections and user queries into vectors</li><li><strong>Vector and lexical search:</strong> Amazon OpenSearch Serverless, to store embeddings and run keyword/vector retrieval</li><li><strong>Answer generation:</strong> Claude through Amazon Bedrock to generate a grounded response from retrieved context</li><li><strong>Agent runtime:</strong> AgentCore Runtime, to host and execute the agent workflow</li><li><strong>Tool integration:</strong> AgentCore Gateway, to expose search capabilities as agent tools</li><li><strong>Conversation state:</strong> AgentCore Memory, to preserve relevant interaction context</li><li><strong>Access control:</strong> AgentCore Identity, to manage identity and authorized tool access</li><li><strong>Monitoring:</strong> AgentCore Observability, to trace agent decisions, tool calls, and failures</li></ul><p>The data source for this project was a collection of product manuals. The manuals were parsed, divided into sections, enriched with metadata such as product name and section title, and indexed in OpenSearch Serverless.</p><h3>Why Keyword Search Alone Was Not Enough</h3><p>Product manuals contain many exact values that are important to preserve:</p><ul><li>Model numbers</li><li>Error codes</li><li>Button names</li><li>Configuration keys</li><li>Firmware versions</li><li>Safety warnings</li></ul><p>For these terms, lexical search is often the right tool. A search for E-1042 should return the section containing E-1042, even if the surrounding language is not semantically similar to the user’s query.</p><p>However, customers do not always use the terminology found in the manual. They may ask:</p><blockquote><em>The screen is on, but the device will not connect to Wi-Fi.</em></blockquote><p>The manual may use terms such as “network pairing failure” or “wireless configuration.” Semantic search helps connect those expressions.</p><p>The key idea is not to choose keyword search or vector search. It is to use each one where it is strongest.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*syPdrMmru5OIkIgR0mt92w.png" /></figure><h3>Architecture</h3><p>The system follows this flow:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Jh2AALS9tU6K0mMgMbiYCQ.png" /></figure><p>The agent receives the user’s question and decides which retrieval capabilities are needed. The search tools query the same OpenSearch Serverless collection, but they use different retrieval modes:</p><ul><li>Lexical search uses the indexed text fields.</li><li>Vector search uses embeddings generated with Amazon Titan Text Embeddings.</li><li>Metadata filters restrict results to the relevant product or manual version.</li></ul><p>The retrieved sections are combined before they are passed to Claude. The model is instructed to answer only from the supplied context and to say when the manuals do not contain enough information.</p><h3>Preparing Product Manuals for Search</h3><p>The ingestion pipeline is:</p><pre>Product manuals<br>    |<br>    v<br>Text extraction<br>    |<br>    v<br>Heading-aware chunking<br>    |<br>    v<br>Metadata enrichment<br>    |<br>    v<br>Amazon Titan Text Embeddings<br>    |<br>    v<br>Amazon OpenSearch Serverless</pre><p>Each chunk contains the text of a manual section together with metadata such as:</p><pre>{<br>  &quot;product&quot;: &quot;Example Product One&quot;,<br>  &quot;manual_version&quot;: &quot;2025.1&quot;,<br>  &quot;section&quot;: &quot;Battery and charging&quot;,<br>  &quot;page&quot;: 42,<br>  &quot;source&quot;: &quot;product-one-manual.pdf&quot;<br>}</pre><p>Metadata is useful for two reasons. It improves relevance by allowing product-specific filtering, and it provides source information that can be shown with the final answer.</p><h4>Choosing the Right Chunk Size</h4><p>Chunking has a direct effect on retrieval quality. A chunk that is too large may contain several unrelated instructions. A chunk that is too small may remove the context needed to understand a warning or procedure.</p><p>For product manuals, heading-aware chunks are more useful than blindly splitting every fixed number of characters. A section such as “Resetting the network connection” should remain connected to its steps, prerequisites, and warnings where possible.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WA_XUIKfAwJP3rIdWNKxUQ.png" /></figure><p>The practical rule I used was to preserve the manual’s logical structure first, then apply size limits and overlap only when required.</p><h3>Combining Lexical and Vector Results with RRF</h3><p>A simple weighted sum is tempting, but raw BM25 scores and vector similarity scores are not necessarily comparable. They can have different scales and distributions.</p><p>For this reason, I used Reciprocal Rank Fusion, or RRF, to combine ranked result lists. RRF uses the position of a document in each result list rather than assuming that the underlying scores are directly comparable.</p><p>A simplified form is:</p><pre>RRF score(document) =<br>    1 / (k + lexical_rank)<br>    + 1 / (k + vector_rank)</pre><p>Here, k is a smoothing constant. A document that appears near the top of both lists receives a strong combined score. Documents that appear in only one list can still be retained, but they are less likely to dominate the final ranking without strong evidence.</p><p>This approach is particularly useful for product manuals because exact error codes can rank highly in lexical search while natural-language descriptions can rank highly in vector search.</p><h3>Routing Queries with AgentCore</h3><p>The search assistant does not treat every question the same way. AgentCore helps coordinate the workflow and expose search operations as tools.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qm1fepIRQ1igtAIJ0ZZDbw.png" /></figure><p>The routing behaviour can be described with three examples:</p><ol><li><strong>Exact error-code query:</strong> route primarily to keyword search.</li><li><strong>Conceptual troubleshooting question:</strong> route primarily to vector search.</li><li><strong>Complex question containing a product name and a natural-language problem:</strong> use both searches and fuse the results.</li></ol><h3>How the AgentCore Components Fit</h3><h4>AgentCore Runtime</h4><p>Runtime hosts the agent execution flow. It provides the environment in which the assistant receives a query, selects tools, retrieves context, and prepares the final model request.</p><h4>AgentCore Gateway</h4><p>Gateway exposes the retrieval operations as tools. Instead of embedding every search operation directly in the agent logic, the workflow can call tools such as:</p><pre>search_manual_keywords(query, product, manual_version)<br>search_manual_semantic(query, product, manual_version)<br>get_manual_section(source, page)</pre><p>This separation makes it easier to add another data source or retrieval capability later.</p><h4>AgentCore Memory</h4><p>Memory preserves useful conversation context. For example, if a user first identifies a product and then asks three follow-up questions, the assistant can use that context without requiring the product name to be repeated in every query.</p><p>Memory should be used carefully. It should not replace authoritative retrieval from the manuals, and stale or irrelevant conversation history should not be allowed to override the current product or manual version.</p><h4>AgentCore Identity</h4><p>Identity controls who can access the agent and which tools or data sources the workflow is authorised to use. In a multi-product or multi-tenant environment, identity and metadata filtering are important safeguards against returning content from the wrong product or customer context.</p><h4>AgentCore Observability</h4><p>Observability makes the agent workflow inspectable. Useful trace information includes:</p><ul><li>The original user query</li><li>The selected tools</li><li>Search latency</li><li>Retrieved document IDs</li><li>Ranking and fusion results</li><li>Model latency</li><li>Errors and fallback paths</li></ul><p>Without this information, it is difficult to tell whether a poor answer came from the model, retrieval, chunking, routing, or missing data.</p><h3>A Bedrock Request Example</h3><p>The following is a simplified implementation example for the product-manual assistant. It demonstrates the shape of a Bedrock Runtime request after the search layer has retrieved relevant manual sections.</p><pre>import json<br>import boto3<br> <br>bedrock = boto3.client(&quot;bedrock-runtime&quot;, region_name=&quot;us-east-1&quot;)<br> <br>MODEL_ID = &quot;anthropic.claude-sonnet-4-6-v1&quot;<br> <br>manual_context = &quot;&quot;&quot;<br>Product: Example Product One<br>Source: product-one-manual.pdf, page 42<br>Section: Battery and charging<br>The device may limit charging to 80% when Battery Protection Mode is enabled.<br>&quot;&quot;&quot;<br> <br>user_question = &quot;Why does my device stop charging at 80%?&quot;<br> <br>prompt = f&quot;&quot;&quot;<br>You are a product support assistant.<br>Answer the question only from the manual context below.<br>If the context is insufficient, say that the manual does not provide enough information.<br>Include the source page in your answer.<br> <br>Manual context:<br>{manual_context}<br> <br>Question:<br>{user_question}<br>&quot;&quot;&quot;<br> <br>response = bedrock.invoke_model(<br>    modelId=MODEL_ID,<br>    body=json.dumps({<br>        &quot;anthropic_version&quot;: &quot;bedrock-2023-05-31&quot;,<br>        &quot;max_tokens&quot;: 500,<br>        &quot;temperature&quot;: 0.1,<br>        &quot;messages&quot;: [<br>            {<br>                &quot;role&quot;: &quot;user&quot;,<br>                &quot;content&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: prompt}]<br>            }<br>        ]<br>    })<br>)<br> <br>result = json.loads(response[&quot;body&quot;].read())<br>print(result[&quot;content&quot;][0][&quot;text&quot;])</pre><blockquote><strong><em>Implementation note:</em></strong><em> Confirm the exact Claude model ID, request schema, and API version for the model and AWS Region you deploy. The snippet is intentionally simplified and is an implementation example, not a copy-paste production integration.</em></blockquote><h3>Evaluation: What to Measure</h3><p>I did not want to evaluate the system only by asking whether an answer sounded good. A useful evaluation set should contain different query types:</p><ul><li>Exact model-number queries</li><li>Exact error-code queries</li><li>Natural-language troubleshooting questions</li><li>Questions requiring metadata filtering</li><li>Questions with no answer in the manuals</li></ul><p>A practical first benchmark could contain 20 manually reviewed questions: 10 exact-term questions and 10 natural-language questions. For each query, compare keyword-only retrieval with the hybrid pipeline using the same top-k value.</p><p>Each configuration (keyword only, vector only, and hybrid with RRF) runs against the same labelled query set. For every query, the evaluation records the retrieved document IDs and their ranks, whether the answer is faithful to the retrieved sections, latency, and token usage.</p><p>Two metrics summarise the comparison: the share of queries with a relevant section in the top 3 results, and the share of answerable questions that are grounded correctly. Questions with no answer in the manuals are scored separately, on whether the assistant correctly says the information is not available.</p><h3>What I Learned</h3><p>The project produced several practical lessons.</p><p>First, retrieval quality often matters more than changing the generation model. If the correct manual section is not retrieved, the model cannot reliably answer from it.</p><p>Second, hybrid search needs a principled fusion method. BM25 and vector scores should not be added together without checking their scales. Rank-based fusion such as RRF is a safer starting point when the retrieval systems produce incompatible scores.</p><p>Third, agents should coordinate capabilities rather than hide the system’s behaviour. The routing decision, tool calls, retrieved context, and fallback path should be observable.</p><p>Finally, product support assistants need to be conservative. A confident but unsupported troubleshooting instruction can be worse than a clear “I could not find that in the manual.”</p><h3>Limitations and Next Steps</h3><p>This project is an implementation study rather than a complete production support platform. The next steps would be to:</p><ul><li>Build a larger labelled evaluation set</li><li>Measure recall, precision, faithfulness, latency, and cost</li><li>Add a real user-interface screenshot and trace examples</li><li>Test prompt-injection content inside uploaded manuals</li><li>Add document version and access-control tests</li><li>Compare different chunking strategies</li><li>Add automated regression tests for retrieval changes</li></ul><h3>Conclusion</h3><p>A product-manual assistant becomes more useful when it combines exact retrieval with semantic understanding. Amazon Titan Text Embeddings and Amazon OpenSearch Serverless provide the search foundation, while Claude on Amazon Bedrock turns retrieved manual sections into a conversational answer.</p><p>AgentCore adds the orchestration layer: Runtime executes the workflow, Gateway exposes tools, Memory preserves useful context, Identity controls access, and Observability makes the decisions and failures traceable.</p><p>The main lesson from this project is that a reliable AI search assistant is not just a model call. It is a system of ingestion, metadata, retrieval, ranking, orchestration, grounding, access control, and evaluation.</p><h3>References</h3><ul><li><a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html">Amazon Titan Text Embeddings models</a></li><li><a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-vector-search.html">Working with vector search collections (Amazon OpenSearch Serverless)</a></li><li><a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html">Anthropic Claude Messages API on Amazon Bedrock</a></li><li><a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/what-is-bedrock-agentcore.html">What is Amazon Bedrock AgentCore?</a></li><li><a href="https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf">Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods (Cormack et al., SIGIR 2009)</a></li></ul><p>If this helped, give it a clap and follow for more practical breakdowns on cloud and AI.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=658357fe808a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/building-a-hybrid-search-assistant-for-product-manuals-with-amazon-bedrock-and-agentcore-658357fe808a">Building a Hybrid Search Assistant for Product Manuals with Amazon Bedrock and AgentCore</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Skip OpenSearch: Build a Serverless Hybrid Search Engine]]></title>
            <link>https://medium.com/awsfullstack/skip-opensearch-build-a-serverless-hybrid-search-engine-4529a99b2353?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/4529a99b2353</guid>
            <category><![CDATA[opensearch]]></category>
            <category><![CDATA[hybrid-search]]></category>
            <dc:creator><![CDATA[Maleesha Nuwanthi Kolombage]]></dc:creator>
            <pubDate>Thu, 12 Nov 2026 11:01:02 GMT</pubDate>
            <atom:updated>2026-11-12T11:01:01.959Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z0YSU7CBw5IU0R9o7EOkFA.png" /></figure><p>The moment a product manager asks for “search functionality”, the default engineering reflex is to provision a dedicated search cluster. We automatically reach for Amazon OpenSearch, Elasticsearch, or a managed vector database.</p><p>But if you are dealing with internal documentation, operational manuals, or specialized internal datasets, provisioning a cluster is often an expensive operational overkill. OpenSearch instances must run 24/7, requiring minimum node instances, networking overhead, index management, and a chunk of your monthly AWS budget, even when idle. What if you could build a production-grade, highly resilient <strong>hybrid (keyword + semantic) search engine</strong> entirely inside a serverless AWS Lambda function for pennies on the dollar?</p><p>Here is the blueprint of a “zero-cluster” search architecture that combines lightning-fast, in-memory JavaScript substring matching with Amazon Bedrock embeddings.</p><h3>The Architecture Overview</h3><p>Instead of maintaining a massive external index, this architecture shifts the heavy lifting to smart application logic and managed serverless primitives. The system uses a two-pass strategy executed inside an AWS Lambda function:</p><pre>[ User Query ]<br>      │<br>      ├──&gt; Pass 1: Local In-Memory Substring Match (DynamoDB Cache)<br>      │             └── (Exact Match Found? -&gt; Return Immediately &amp; Save Cost)<br>      │<br>      └──&gt; Pass 2: Semantic Vector Match (If Keyword Pass Isn&#39;t Absolute)<br>                    ├── Embed query via Amazon Bedrock (Titan V2)<br>                    ├── Query S3 Global/Org Vector Indexes<br>                    └── Map hits to DB records via Custom Resolver</pre><h3>Pass 1: The High-Speed Keyword Bypass</h3><p>The first layer of defense is pure, deterministic execution. The system fetches manual data from Amazon DynamoDB via a Global Secondary Index ( manualsByIsLatest ). Instead of querying the database on every single keystroke, the Lambda function implements dual-tier in-memory caching which are;</p><ul><li><strong>Metadata Cache (5 Seconds):</strong> Names, normalized names, and categories are cached briefly to capture fast operational updates.</li><li><strong>Content Cache (5 Minutes):</strong> Heavy text bodies are cached longer to protect the Lambda from data-transfer bottlenecks and eliminate DynamoDB read-capacity spikes.</li></ul><p>When a query comes in, the code lowercases the input and uses native JavaScript .includes() to check against the manual’s attributes.</p><h4><strong>The Optimization Trick:</strong></h4><p>The content body check only triggers if the query is at least 3 characters long. Furthermore, if the query returns an exact match against a document’s name or category, the engine assumes absolute intent. It returns the result immediately and skips the semantic vector pass entirely, saving execution time and AI token costs.</p><h3>Pass 2: The Serverless Semantic Layer</h3><p>If the keyword match isn’t an exact, definitive shortcut, the engine seamlessly initiates the semantic pass.</p><ol><li><strong>Query Embedding:</strong> The search string is embedded using Amazon Bedrock with the <em>amazon.titan-embed-text-v2:0</em> model.</li><li><strong>S3 Vector Indexing:</strong> Instead of querying a running vector database, the application queries pre-computed vector files hosted on Amazon S3. It targets two distinct index scopes, a global index and an organization-specific index ( org-{organizationId} ), restricted by the caller&#39;s department permissions.</li><li><strong>Distance Pruning:</strong> The engine retrieves the top 10 matches and applies a mathematical threshold, dropping any match with a cosine distance above 0.5.</li></ol><h3><strong>The Identity Problem: Reverse-Mapping</strong></h3><p>Vectors don’t naturally understand your database primary keys, they return abstract file chunks or URLs. To solve this, a dedicated resolver ( manual-vector-resolver.ts ) maps the vector matches back to primary DynamoDB keys by cross-referencing deterministic fields like the PDF source URL, editor key, or normalized name.</p><h3>Solution for Resiliency: The Graceful Fallback</h3><p>Relying on external AI APIs introduces risk. Bedrock can hit rate limits, network blips happen, or S3 index reads can occasionally time out.</p><p>To handle this, the search layer wraps the entire hybrid pipeline in a robust fail-safe. If the Bedrock embedding or vector retrieval throws an error, the engine gracefully downgrades to the raw in-memory substring match. The user might lose semantic understanding for a split second, but they are never met with a blank screen or a 500 Internal Server Error.</p><h3>Designing for Distinct Contexts</h3><p>One size rarely fits all. In a production ecosystem, you might need this high-speed manual locator to coexist with a broader Document Library search ( document-library-search.ts ).</p><p>While your operational manual search prioritizes exact text containment and quick shortcuts, a deeper document library search can run an entirely different algorithmic blend, matching whole words with simple word stemming, treating multi-word phrases differently than single terms, and applying stricter distance cut-offs to keep massive archives clean.</p><h3><strong>What about DynamoDB’s native vector search?</strong></h3><p>While AWS’s introduction of native vector support inside Amazon DynamoDB eliminates the “Identity Problem” by allowing you to query embeddings directly inside your database records, it does not make this “zero-cluster” blueprint obsolete. Instead, it presents a classic trade-off between <strong>operational convenience and extreme cost-efficiency</strong>. Switching to native vectors simplifies your pipeline by removing custom S3 sync scripts, but it introduces persistent database indexing costs. More importantly, native database features cannot replace full-stack application logic, even with native vectors, Pass 1 (The Keyword Bypass) remains essential. Without this local, in-memory JavaScript layer protecting your pipeline, every single search would still force a costly network round-trip and API token charge to Amazon Bedrock just to embed the query. Stick to the S3 flat-file approach for absolute zero-dollar idle infrastructure and strict multi-tenant isolation, or adopt native DynamoDB vectors if you prefer maximum engineering simplicity and are willing to pay a minor baseline premium to completely offload index management.</p><h3>Summary: When to Go Clusterless</h3><p>This architecture proves that you don’t always need to inherit the operational baggage of a dedicated search engine cluster to deliver smart search results.</p><p><strong>Go Serverless/Clusterless if:</strong></p><ul><li>Your dataset is bounded (e.g., thousands of highly detailed manuals rather than millions of retail products).</li><li>You are operating on a strict budget and want near-zero idle infrastructure costs.</li><li>You want absolute control over caching and algorithmic fallbacks using standard JavaScript.</li></ul><p><strong>Stick to OpenSearch if:</strong></p><ul><li>You are searching across millions of rapidly mutating database rows.</li><li>You require complex native text analytics like tokenization, fuzzy phonetic matching, or complex multi-language stemming out of the box.</li></ul><p>By letting application memory handle the obvious matches and using serverless embeddings for the conceptual ones, you can build a fast, incredibly cheap, and maintenance-free search layer that scales perfectly with your traffic.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4529a99b2353" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/skip-opensearch-build-a-serverless-hybrid-search-engine-4529a99b2353">Skip OpenSearch: Build a Serverless Hybrid Search Engine</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fixing the Circular Dependency Error When a Function Calls Your Data API]]></title>
            <link>https://medium.com/awsfullstack/fixing-the-circular-dependency-error-when-a-function-calls-your-data-api-90a0a2757a57?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/90a0a2757a57</guid>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[aws-amplify]]></category>
            <category><![CDATA[aws-cloudformation]]></category>
            <dc:creator><![CDATA[Poorna Theekshana]]></dc:creator>
            <pubDate>Thu, 12 Nov 2026 05:31:01 GMT</pubDate>
            <atom:updated>2026-11-12T05:31:01.567Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2QPOPeN-0gp46Hx9z-s0hg.png" /></figure><p>Custom business logic is one of the nicer parts of Amplify Gen 2. You add a placeOrder mutation to your schema, point it at a Lambda function, write a few lines of TypeScript, and the sandbox deploys it in a couple of minutes. The frontend calls it like any other operation.</p><p>Then the function needs to do a little more. Before it creates an order, it should check that the product exists and has stock. That means reading another model, so you give the function access to the data API with one line in the schema, save, and wait for the sandbox.</p><p>This time it does not deploy:</p><pre>The CloudFormation deployment failed due to circular dependency found between nested stacks [data1234ABCD, function6789XYZ]</pre><p>Nothing in your handler is wrong, and nothing in the schema is invalid. The two lines you wrote are both correct on their own. Together, they ask CloudFormation for something it cannot do.</p><p>In this article, we will look at what the Amplify Gen 2 circular dependency error is actually describing, why this particular pair of lines causes it, and the one-property fix that the Amplify docs recommend.</p><h3>What a Nested Stack Is (In Simple Terms)</h3><p>Amplify Gen 2 turns your amplify/ folder into a CloudFormation deployment. Each part of the backend lands in its own nested stack, which is why the error names stacks like data1234ABCD and function6789XYZ. The suffix changes from app to app. The prefix is the part that matters.</p><p>Functions have a default home. As the <a href="https://docs.amplify.aws/react/build-a-backend/functions/configure-functions/">Configure Functions</a> page puts it, “By default, functions are grouped together in a resource group named function.&quot; Your data API lives in the data group.</p><p>Nested stacks are not isolated. When one needs a value from another, such as an ARN or an endpoint URL, CloudFormation has to finish the stack that produces the value before it can start the one that consumes it. That gives every reference a direction, and the stacks a deploy order.</p><p>An Amplify CloudFormation circular dependency is what you get when those directions form a loop. If data must wait for function, and function must wait for data, neither can go first.</p><h3>Two Arrows Pointing at Each Other</h3><p>Here is the setup after that last change. The storage definition registers the function as its upload trigger and gives it access to two folders:</p><pre>// amplify/storage/resource.ts<br>import { defineStorage } from &#39;@aws-amplify/backend&#39;;<br>import { processUpload } from &#39;./process-upload/resource&#39;;<br><br>export const storage = defineStorage({<br>  name: &#39;uploads&#39;,<br>  triggers: {<br>    onUpload: processUpload, // storage → function<br>  },<br>  access: (allow) =&gt; ({<br>    &#39;uploads/*&#39;: [allow.resource(processUpload).to([&#39;read&#39;])],<br>    &#39;thumbnails/*&#39;: [allow.resource(processUpload).to([&#39;write&#39;])],<br>  }),<br>});</pre><p>Then, in backend.ts, the function is given the bucket name so it knows where to write the thumbnail:</p><pre>// amplify/backend.ts<br>import { defineBackend } from &#39;@aws-amplify/backend&#39;;<br>import { storage } from &#39;./storage/resource&#39;;<br>import { processUpload } from &#39;./storage/process-upload/resource&#39;;<br>const backend = defineBackend({ storage, processUpload });<br>backend.processUpload.addEnvironment(<br> &#39;THUMBNAIL_BUCKET&#39;,<br> backend.storage.resources.bucket.bucketName, // function → storage<br>);</pre><p>Two lines create the two arrows.</p><p>onUpload: processUpload makes the function a <a href="https://docs.amplify.aws/react/build-a-backend/storage/lambda-triggers/">storage trigger</a>. S3 has to know which Lambda function to call when an object lands in the bucket, so the storage stack needs the function&#39;s ARN from the function stack.</p><p>addEnvironment(...) goes the other way. The variable holds a value that only exists once the bucket has been created, so the function stack now needs the bucket name from the storage stack.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2cYHmKU39Vp9jLvmbAhl9A.png" /></figure><p>The first deploy worked because only the trigger existed. Adding the environment variable closed the loop.</p><p>The access rules are not part of the problem. According to the <a href="https://docs.amplify.aws/react/build-a-backend/storage/authorization/">storage authorization docs</a>, granting access with allow.resource() already gives the function an environment variable named uploads_BUCKET_NAME, and Amplify wires that reference in one direction only. The hand-written THUMBNAIL_BUCKET duplicates something the function already had.</p><h3>The Obvious Fix Isn’t the Right One</h3><p>The quick way out is to remove one arrow. Drop allow.resource(), copy the GraphQL endpoint from amplify_outputs.json into an environment variable, and handle authorization in the function yourself.</p><p>It deploys, and it is a trap. The endpoint is different in every sandbox and every branch environment, so the value you pasted points at one specific backend. The next teammate who starts their own sandbox has a function quietly writing to yours. You have also traded access that Amplify scopes and grants for credentials you now manage by hand.</p><p>Deploying in two passes, first without the access rule and then with it, has the same weakness. The second pass still produces the loop, and a fresh environment has to repeat the dance every time.</p><p>Both arrows are legitimate. The fix is to stop them crossing a stack boundary.</p><h3>The Fix: Put the Function in the Data Stack</h3><p>Open the function’s resource.ts and add one property:</p><pre>// amplify/data/place-order/resource.ts<br>import { defineFunction } from &#39;@aws-amplify/backend&#39;;<br><br>export const placeOrder = defineFunction({<br>  name: &#39;place-order&#39;,<br>  resourceGroupName: &#39;data&#39;,<br>});</pre><p>Setting the Amplify function resourceGroupName to &#39;data&#39; is exactly what the <a href="https://docs.amplify.aws/react/build-a-backend/troubleshooting/circular-dependency/">circular dependency troubleshooting page</a> recommends for this case: &quot;if you are using the function as a query handler, but the function also needs access to the data (or AppSync) API, you might run into this issue. To resolve, group this function with other resources in the data stack.&quot;</p><p>The handler itself does not change. It configures the data client from the function’s environment and then works with models as usual:</p><pre>// amplify/data/place-order/handler.ts<br>import type { Schema } from &#39;../resource&#39;;<br>import { Amplify } from &#39;aws-amplify&#39;;<br>import { generateClient } from &#39;aws-amplify/data&#39;;<br>import { getAmplifyDataClientConfig } from &#39;@aws-amplify/backend/function/runtime&#39;;<br>import { env } from &#39;$amplify/env/place-order&#39;;<br><br>const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(env);<br>Amplify.configure(resourceConfig, libraryOptions);<br>const client = generateClient&lt;Schema&gt;();<br>export const handler: Schema[&#39;placeOrder&#39;][&#39;functionHandler&#39;] = async (event) =&gt; {<br>  const { productId, quantity } = event.arguments;<br>  const { data: product } = await client.models.Product.get({ id: productId });<br>  if (!product || product.stock &lt; quantity) {<br>    throw new Error(&#39;Product unavailable&#39;);<br>  }<br>  const { data: order, errors } = await client.models.Order.create({ productId, quantity });<br>  if (!order) {<br>    throw new Error(errors?.[0]?.message ?? &#39;Order was not created&#39;);<br>  }<br>  return order;<br>};</pre><p>A few lines are worth reading closely:</p><ul><li>name: &#39;place-order&#39; must match the import path $amplify/env/place-order, because that module is generated per function name.</li><li>getAmplifyDataClientConfig(env) is where the function picks up the API details that allow.resource() granted. No endpoint is hard-coded anywhere.</li><li>Schema[&#39;placeOrder&#39;][&#39;functionHandler&#39;] types both the arguments and the return value against the mutation in your schema.</li></ul><p>Why does this work? Both references still exist. They just no longer cross between two nested stacks. With the function inside data, there is no second stack to wait for, so there is no stack-level loop to report.</p><p>To verify, save the file and let npx ampx sandbox redeploy. The deployment should complete without the circular dependency message. Then call the mutation from a signed-in client and confirm an Order comes back. Any error you hit now comes from your handler, which is a much better problem to have.</p><blockquote><em>If a function serves a resource and also calls it, it belongs in that resource’s stack.</em></blockquote><h3>Reading the Error Before Changing Anything</h3><p>resourceGroupName: &#39;data&#39; fixes the data case. It is not a universal answer, and the error message tells you which case you are in. The names in the brackets are the stacks involved.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DtC1jsRWOdqRo7v6K8MTIw.png" /></figure><p>The <a href="https://docs.amplify.aws/react/build-a-backend/troubleshooting/circular-dependency/">same troubleshooting page</a> covers the other shapes. A function used as an auth trigger can be grouped with resourceGroupName: &#39;auth&#39;. A custom CDK resource that loops with an Amplify stack should be created inside that Amplify stack, for example new sqs.Queue(backend.storage.stack, &#39;MyCustomQueue&#39;), rather than in a stack of its own. If the error names resources inside a single stack instead of nested stacks, the docs point to the AWS post on <a href="https://aws.amazon.com/blogs/infrastructure-and-automation/handling-circular-dependency-errors-in-aws-cloudformation/">handling circular dependency errors in CloudFormation</a>.</p><h3>When One Group Is Not Enough</h3><p>Because every function shares the function group by default, two unrelated functions can combine into a loop that neither causes alone. One is an auth trigger, another reads from data, and the shared stack now sits between auth and data in both directions. In that case, look at each function in the bracketed stack and move the ones with cross-stack references, either into the stack they depend on or into their own group, which resourceGroupName also allows.</p><p>If the error survives all of that, the docs ask you to open an issue on the <a href="https://github.com/aws-amplify/amplify-backend/issues/new/choose">amplify-backend repository</a> with the full message.</p><p>It is also worth asking whether the logic needs a function at all. If the handler only reads or writes your own tables, an AppSync JavaScript resolver may do the job with no function stack involved. We covered that path in <a href="https://awsfullstack.com/posts/how-to-get-started-with-javascript-resolvers-in-aws-amplify-gen-2">How to Get Started with JavaScript Resolvers in AWS Amplify Gen 2</a>.</p><h3>Final Thoughts</h3><p>The Amplify Gen 2 circular dependency error looks like infrastructure going wrong, but it is describing your design accurately. A function that is both a handler for the data API and a client of it really does depend on data in both directions. Amplify cannot deploy that across two stacks, so you put both sides in one.</p><p>The rule carries over to every resource group: find the stacks named in the error, and group the function with the resource it serves. A function that only calls other resources, without serving any of them, can stay in the default function group, as long as it does not share that group with a function pointing the other way.</p><h3>FAQ</h3><h4>What causes the circular dependency error in Amplify Gen 2?</h4><p>A function that is both the handler for a custom query or mutation and granted access to the data API with allow.resource(). The data stack needs the function, the function stack needs the API, and CloudFormation cannot deploy either one first.</p><h4>Can I hard-code the GraphQL endpoint instead?</h4><p>It deploys, and it breaks later. The endpoint is different in every sandbox and branch environment, and you lose the access Amplify scopes and grants for you.</p><h4>Do I need to change the handler code after moving the function?</h4><p>No. The handler still reads the API details through getAmplifyDataClientConfig(env). Only the function&#39;s resource.ts changes.</p><h4>What if the error names the auth stack instead of data?</h4><p>Group the function with auth when it is an auth trigger, using resourceGroupName: &#39;auth&#39;. For a custom CDK resource, create it inside the Amplify stack it depends on.</p><p>Next in this series: t<a href="https://medium.com/awsfullstack/api-key-not-found-on-sandbox-redeploy-using-sandbox-identifiers-0ab356068a73">he “API key not found” error on a sandbox redeploy, and how sandbox identifiers keep environments from stepping on each other.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=90a0a2757a57" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/fixing-the-circular-dependency-error-when-a-function-calls-your-data-api-90a0a2757a57">Fixing the Circular Dependency Error When a Function Calls Your Data API</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS Lambda Durable Functions: Steps, Checkpoints, Replay, and Real-World Workflows]]></title>
            <link>https://medium.com/awsfullstack/aws-lambda-durable-functions-steps-checkpoints-replay-and-real-world-workflows-506f4f703821?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/506f4f703821</guid>
            <category><![CDATA[durable-functions]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 12:24:12 GMT</pubDate>
            <atom:updated>2026-11-10T14:33:39.431Z</atom:updated>
            <content:encoded><![CDATA[<h4>A practical guide to steps, checkpoints, replay, and retries, with a real data-pipeline example</h4><p>Serverless is great at small, event-driven jobs. It gets harder when the job turns into a <strong>workflow</strong>: several steps, calls to flaky external APIs, a wait of a few hours, and maybe a human approval in the middle. <strong>AWS Lambda Durable Functions</strong> let you write that kind of workflow as ordinary TypeScript or Python, while AWS handles <strong>checkpointing, replay, suspension, and recovery</strong> for you.</p><p>I’ve run Azure Durable Functions in production, and I’ve built scraping and data pipelines where something fails halfway through every day. So this isn’t a rewrite of the AWS docs. It covers how the model actually works, where it can catch you out, and how I’d use it on a real vehicle-data pipeline.</p><h3>What a “workflow” used to look like on Lambda</h3><p>Take an ordinary order flow:</p><pre>Receive order → Validate → Reserve inventory → Take payment → Wait for shipment → Notify customer</pre><p>Now add failure. Payment times out. The inventory service is down for two minutes. Shipping confirmation arrives six hours later. A manager has to approve a refund.</p><p>A single Lambda invocation can’t cope with that. It has a <strong>15-minute maximum</strong> and forgets everything when it ends. So we usually build the durability ourselves:</p><ul><li><strong>DynamoDB</strong> to store which step we’re on</li><li><strong>SQS or EventBridge</strong> to hand work between functions</li><li><strong>Several Lambdas</strong>, one per step</li><li><strong>Custom retry logic</strong> and dead-letter queues</li><li><strong>Scheduled events</strong> to “come back later”</li><li><strong>Idempotency checks</strong> so retries don’t double-charge anyone</li></ul><p>None of these are bad building blocks. The problem is that by the end, <strong>you’ve written your own workflow engine</strong>, and the business logic is scattered across five services.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BOh3VusQgKBXbfq8jeqUEA.png" /></figure><p>Durable functions turn that around. The workflow lives in one function’s code, and Lambda keeps track of where it got to.</p><h3>The short version</h3><ul><li><strong>A durable function is still a Lambda function.</strong> You wrap the handler with the SDK and get a DurableContext.</li><li><strong>A durable execution can run for up to one year.</strong> Each invocation inside it still has the normal 15-minute limit.</li><li><strong>Steps are checkpointed.</strong> Once a step finishes, its result is saved.</li><li><strong>Recovery works by replay.</strong> The handler runs again from the top, and completed steps return their saved results instead of running again.</li><li><strong>Waits cost no compute.</strong> The function suspends, Lambda exits, and AWS resumes it later.</li><li><strong>Retries are built into steps,</strong> with backoff, and the retry delays don’t use execution time either.</li><li><strong>Your orchestration code must be deterministic.</strong> This is the main new thing to learn.</li><li><strong>Step Functions are still useful.</strong> The two tools fit different jobs, and they work well together.</li></ul><h3>What are AWS Lambda Durable Functions?</h3><p>A durable function is a regular Lambda function with <strong>durable execution</strong> enabled. AWS provides the <strong>Durable Execution SDK</strong> for JavaScript/TypeScript, Python, and Java. For TypeScript:</p><pre>npm install @aws/durable-execution-sdk-js</pre><p>A minimal durable function looks like this:</p><pre>import { DurableContext, withDurableExecution } from &quot;@aws/durable-execution-sdk-js&quot;;<br><br>export const handler = withDurableExecution(<br>  async (event: any, context: DurableContext) =&gt; {<br>    const order = await context.step(&quot;validate-order&quot;, async () =&gt;<br>      validateOrder(event.orderId)<br>    );<br><br>    const payment = await context.step(&quot;process-payment&quot;, async () =&gt;<br>      processPayment(order)<br>    );<br><br>    await context.wait({ minutes: 30 });<br><br>    return await context.step(&quot;confirm-order&quot;, async () =&gt;<br>      confirmOrder(order, payment)<br>    );<br>  }<br>);</pre><p>It reads like normal async code, and that’s intentional. There’s no state machine JSON and no separate workflow designer. <strong>The workflow is the code.</strong></p><p>One point people often miss: “runs for a year” <strong>does not</strong> mean a Lambda container stays alive for a year. A durable execution is a series of short invocations with saved progress in between:</p><pre>Invocation 1 → run steps → checkpoint → exit<br>      ... hours pass (no compute billed) ...<br>Invocation 2 → replay → run next step → checkpoint → exit<br>      ... days pass ...<br>Invocation 3 → replay → finish → result</pre><h3>Steps: the unit of durable work</h3><p>A <strong>step</strong> wraps work that should happen once and be remembered: an API call, a database write, a calculation you don’t want to repeat.</p><pre>const customer = await context.step(&quot;load-customer&quot;, async () =&gt;<br>  loadCustomer(event.customerId)<br>);</pre><p>When the step finishes, the SDK <strong>checkpoints its result</strong>. If the function is replayed later, context.step(&quot;load-customer&quot;) doesn&#39;t call loadCustomer again. It returns the stored value straight away.</p><p>Two habits that pay off:</p><ul><li><strong>Name every step.</strong> Names make execution history readable, and they help the runtime match saved state to your code.</li><li><strong>Never rename or reorder steps while executions are in flight.</strong> AWS warns that this can stop running executions from resuming or make them produce wrong results. More on this in the production section.</li></ul><h3>Checkpoint + replay: the core idea</h3><p>This is the concept everything else depends on, so it’s worth going slowly.</p><p>Say a workflow has three steps. Steps A and B succeed. Then the environment goes away before Step C finishes. Maybe the invocation hit its timeout, or maybe it suspended on purpose for a wait.</p><p>When the execution resumes, AWS <strong>runs your handler again from the first line</strong>:</p><pre>Replay → Step A  →  already done, return saved result<br>       → Step B  →  already done, return saved result<br>       → Step C  →  not done, execute it now</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ps6QhZ_1KxmQsga8kns4Ig.png" /></figure><p>That’s the whole trick. The function doesn’t jump to line 47. It <strong>replays quickly through recorded history</strong> until it reaches work that hasn’t happened yet.</p><p>This has one big consequence:</p><blockquote><strong><em>Your orchestration code describes the workflow. Durable operations do the work.</em></strong></blockquote><p>Anything outside a step runs again on <strong>every replay</strong>. So code like this has a hidden problem:</p><pre>const id = crypto.randomUUID();   // different value on every replay<br>const now = Date.now();           // different value on every replay<br><br>if (now &gt; deadline) { ... }       // the workflow can take a different path</pre><p>If a replay generates a different ID or reads a different time, the workflow can take a different branch than it did the first time. Then the recorded history no longer matches the code. The fix is to put anything non-deterministic <strong>inside a step</strong>, so its result gets recorded once:</p><pre>const id = await context.step(&quot;generate-id&quot;, async () =&gt; crypto.randomUUID());</pre><p>The same goes for random values, the current time, network calls, and database reads. Logging is a milder case. A console.log outside a step just prints again on every replay. That&#39;s harmless but confusing when you&#39;re debugging, so use the step context&#39;s logger for anything that matters.</p><p>If you’ve written Azure Durable Functions orchestrators, this will feel familiar: it’s the same <strong>deterministic orchestrator</strong> rule, just in a new place.</p><h3>Waits that don’t cost compute</h3><p>Long workflows spend most of their time waiting. Durable Functions provide context.wait():</p><pre>// unnamed<br>await context.wait({ hours: 2 });<br><br>// named (easier to spot in execution history)<br>await context.wait(&quot;cool-down-before-recheck&quot;, { minutes: 30 });</pre><p>When the function reaches a wait, the SDK checkpoints, <strong>the invocation ends</strong>, and AWS starts a new invocation when the time is up. You don’t pay for Lambda execution time while it waits, so a two-minute wait and a two-week wait cost about the same.</p><p>That makes waits a good fit for:</p><ul><li><strong>Polling</strong> an external system every N minutes</li><li><strong>Delayed actions</strong> like reminders or follow-up emails</li><li><strong>Cool-down periods</strong> when a third-party API rate-limits you</li></ul><h4>Waiting for a human (or another system)</h4><p>Sometimes you’re waiting for an <em>event</em>, not a time. A manager approves a refund. A partner system confirms a shipment. For this, the SDK has <strong>callbacks</strong>:</p><pre>const decision = await context.waitForCallback(<br>  &quot;wait-for-approval&quot;,<br>  async (callbackId) =&gt; {<br>    await sendApprovalRequest(callbackId, event.requestId);<br>  }<br>);</pre><p>The function suspends until an external system calls the Lambda API (SendDurableExecutionCallbackSuccess or ...Failure) with that callback ID. Before this, you&#39;d build approval flows with a DynamoDB table and a token. Now they take a few lines.</p><h3>Built-in retries (and why idempotency still matters)</h3><p>Distributed systems fail in boring, temporary ways: 503 Service Unavailable, timeouts, throttling. Usually you want to retry the one thing that failed, not restart the whole workflow.</p><p>Steps accept a retry strategy:</p><pre>import { createRetryStrategy } from &quot;@aws/durable-execution-sdk-js&quot;;<br><br>const retryStrategy = createRetryStrategy({<br>  maxAttempts: 5,<br>  initialDelay: { seconds: 2 },<br>  maxDelay: { minutes: 1 },<br>  backoffRate: 2,<br>});<br><br>const result = await context.step(<br>  &quot;call-external-api&quot;,<br>  async () =&gt; callExternalApi(),<br>  { retryStrategy }<br>);</pre><p>A few details worth knowing:</p><ul><li><strong>Retry delays suspend the function too.</strong> The SDK checkpoints the error, exits, and resumes when the next attempt is due. Backoff doesn’t burn execution time.</li><li><strong>There’s a default.</strong> With no strategy at all, steps are already retried several times. Set one explicitly so the behavior is clear in code review.</li><li><strong>You can filter what’s retryable</strong> with retryableErrors and retryableErrorTypes. A 400 Bad Request should fail fast, not be retried five times.</li><li><strong>The execution itself isn’t retried automatically</strong> if it fails completely. For async invocations, configure a <strong>dead-letter queue</strong>.</li></ul><p>Retries bring back a classic distributed-systems problem: <strong>idempotency</strong>. A step can run more than once. For example, the payment API might succeed, and then the network drops before the checkpoint is saved. If the step creates a payment, a retry could create a second one.</p><pre>First attempt  → payment created → checkpoint lost<br>Retry          → payment created AGAIN  ❌</pre><p>The fix is the same as anywhere else. Send an <strong>idempotency key</strong> (such as the order ID) so the downstream service sees a repeat request and returns the original result. Durable execution makes reliable workflows much easier to build, but <strong>it doesn’t replace good distributed-systems design</strong>.</p><h3>A real-world example: a vehicle-data pipeline</h3><p>This is where it gets personal. I’ve built scraping pipelines for vehicle marketplace data, and they’re a good stress test for any workflow tool. They involve:</p><ul><li><strong>Thousands of items</strong> per run</li><li><strong>Pages that fail</strong> in unpredictable ways</li><li><strong>Rate limits</strong> that force you to slow down</li><li><strong>Steps that are expensive to repeat</strong>, such as rendering a page or calling an enrichment API</li></ul><p>The pipeline looks like this:</p><pre>Start job → Fetch listings → For each vehicle: scrape → normalize → validate → store<br>          → Calculate derived properties → Publish results</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*luPDli1e0FWDB6J0o4g7Gg.png" /></figure><p>The old version used SQS between every stage, a DynamoDB table of job state, and a fair amount of glue code. As a durable function, it fits in one handler:</p><pre>export const handler = withDurableExecution(<br>  async (event, context: DurableContext) =&gt; {<br><br>    const listingIds = await context.step(&quot;fetch-listings&quot;, async () =&gt;<br>      fetchListingIds(event.marketplace)<br>    );<br><br>    const results = await context.map(<br>      &quot;process-vehicles&quot;,<br>      listingIds,<br>      async (ctx, listingId) =&gt; {<br>        const raw = await ctx.step(`scrape-${listingId}`, async () =&gt;<br>          scrapeVehicle(listingId), { retryStrategy }<br>        );<br>        const vehicle = await ctx.step(`normalize-${listingId}`, async () =&gt;<br>          normalizeVehicle(raw)<br>        );<br>        return await ctx.step(`store-${listingId}`, async () =&gt;<br>          storeVehicle(vehicle)   // returns an ID/S3 key, not the full record<br>        );<br>      },<br>      { maxConcurrency: 20 }<br>    );<br><br>    await context.step(&quot;calculate-derived&quot;, async () =&gt;<br>      calculateDerivedProperties(results.getResults())<br>    );<br><br>    await context.step(&quot;publish&quot;, async () =&gt; publishResults(event.marketplace));<br>    return { status: &quot;completed&quot; };<br>  }<br>);</pre><p>What I like about this:</p><ul><li><strong>context.map processes vehicles in parallel</strong> with a concurrency cap. That&#39;s important when the marketplace rate-limits you.</li><li><strong>Each vehicle has its own checkpointed steps.</strong> If vehicle #4,812 fails, the other items keep going, and only that item’s failed step retries.</li><li><strong>If the whole thing is interrupted, it resumes.</strong> Vehicles that are already finished aren’t scraped again on replay.</li><li><strong>One file shows the whole pipeline.</strong> A new engineer can read it top to bottom.</li></ul><p>And the lesson I’d pass on from experience: <strong>keep the checkpointed state small.</strong> Every step result is stored in the execution history, so don’t return a 2 MB HTML page from a step. Write large payloads to S3 and return the key. AWS calls this out in its own best practices, because large state costs more and slows replay down.</p><h3>Durable Functions vs AWS Step Functions</h3><p>This is usually the first question: <em>“AWS already has Step Functions. Why do I need this?”</em></p><p>They overlap, but they’re built for different jobs. AWS’s own framing is roughly: <strong>Durable Functions are for application development inside Lambda. Step Functions are for workflow orchestration across AWS.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tNiYS_z8NouTBbVqp-aLrQ.png" /></figure><p><strong>Choose Durable Functions when:</strong></p><ul><li><strong>Most of your logic already lives in Lambda,</strong> and you want it in one codebase.</li><li><strong>Your team prefers TypeScript, Python, or Java</strong> over a JSON/ASL state machine.</li><li><strong>You want normal developer tooling</strong>: unit tests, debuggers, code review, and refactoring.</li><li><strong>You need fine-grained control</strong> over branching, loops, and error handling in code.</li></ul><p><strong>Choose Step Functions when:</strong></p><ul><li><strong>You orchestrate many AWS services</strong> and want native integrations (200+ services) without writing SDK glue.</li><li><strong>Non-engineers need to see the workflow,</strong> and a visual graph helps with cross-team sign-off.</li><li><strong>You want fully managed orchestration</strong> with no runtime or dependency updates on your side.</li></ul><p><strong>Or use both.</strong> A sensible architecture lets Step Functions coordinate the big cross-service picture (ingestion, ECS jobs, Bedrock calls, notifications), while durable Lambdas handle the detailed application logic inside each stage. AWS explicitly recommends this hybrid approach.</p><h3>Coming from Azure Durable Functions</h3><p>If you’ve used Azure Durable Functions, you already know most of this. Both are built on the same ideas:</p><ul><li><strong>Orchestration in code</strong>, with activities/steps doing the real work</li><li><strong>Event-sourced history</strong> with checkpoint and replay</li><li><strong>Durable timers</strong> and <strong>external events/callbacks</strong></li><li><strong>The deterministic-orchestrator rule</strong></li></ul><p>The differences are mostly in packaging. Azure has separate orchestrator and activity functions, built on the Durable Task Framework and a storage provider. AWS keeps everything in <strong>one Lambda handler</strong>, with steps as inline closures, and AWS manages the state store for you. That means less wiring, but also less visibility into the internals.</p><p>My advice to anyone moving between clouds: <strong>learn the model, not the API names.</strong> Once replay makes sense to you, either platform takes about an afternoon to pick up.</p><h3>Production considerations</h3><p>These are the things the getting-started guides tend to rush past.</p><p><strong>1. Durability is set at creation time.</strong> You configure DurableConfig (with a required ExecutionTimeout, optional RetentionPeriodInDays and an optional KMS key) <strong>when you create the function</strong>. You can&#39;t turn durable execution on for an existing function. So include it in your infrastructure-as-code from the start.</p><p><strong>2. Don’t run durable executions on </strong><strong>$LATEST.</strong> This one catches people. An execution that started last Tuesday might replay against code you deployed this morning. If the new code has renamed a step, added one, or reads stored results differently, the replay no longer matches the history. Always invoke durable functions through a <strong>published version or alias</strong>, and make sure new code can still handle state from older executions.</p><blockquote><em>With a normal Lambda, a deploy affects the next request. With a durable workflow that lives for weeks, </em><strong><em>versioning becomes part of correctness</em></strong><em>.</em></blockquote><p><strong>3. Keep state lean.</strong> Return IDs and S3 keys from steps, not large objects.</p><p><strong>4. Plan for whole-execution failure.</strong> Configure a DLQ for async invocations, and decide what “give up” means for your business process.</p><p><strong>5. Monitor executions, not just invocations.</strong> One business transaction is now many invocations. Use the execution history APIs and CloudWatch to trace a workflow from start to finish.</p><h3>When should you (not) use them?</h3><p><strong>Good fit:</strong></p><ol><li><strong>Multiple sequential steps</strong> where repeating finished work is expensive or unsafe? → Yes.</li><li><strong>Long pauses</strong> (minutes to months) between steps? → Yes.</li><li><strong>Human approval</strong> or waiting on an external system? → Yes, use callbacks.</li><li><strong>Flaky external dependencies</strong> that need retry with backoff? → Yes.</li><li><strong>Large batches</strong> that need parallel processing and partial recovery? → Yes, use map.</li></ol><p><strong>Probably not:</strong></p><ul><li><strong>A simple S3 event → resize image → done.</strong> A plain Lambda is fine. Don’t add replay semantics you don’t need.</li><li><strong>Orchestrating ten AWS services for a non-technical audience.</strong> Step Functions will serve you better.</li></ul><p>The architecture should follow the problem, not the latest launch.</p><h3>Final thoughts</h3><p>The interesting part of Lambda Durable Functions isn’t that Lambda can now “run for a year”. It’s that <strong>durability is now part of the programming model</strong>.</p><p>Real applications are workflows. They wait. They retry. They call unreliable systems. They need approvals. They fail halfway through and have to carry on tomorrow. We used to model all of that with queues, tables, and glue code. Now we can write it as code that reads like the business process.</p><p>The infrastructure hasn’t gone away. AWS now runs it, so we can focus on the workflow itself.</p><p>So the useful question isn’t <em>“Should I use Durable Functions?”</em> It’s this:</p><blockquote><strong><em>“Where should the state and orchestration of this workflow live?”</em></strong></blockquote><p>Answer that honestly, and the right tool, whether it’s Lambda, Durable Functions, Step Functions, or a mix, usually becomes clear.</p><h3>References</h3><ul><li><a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html">AWS Lambda durable functions</a></li><li><a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-getting-started.html">Getting started with durable functions</a></li><li><a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-configuration.html">Configure Lambda durable functions</a></li><li><a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-best-practices.html">Durable functions best practices</a></li><li><a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-step-functions.html">Durable functions vs Step Functions</a></li><li><a href="https://docs.aws.amazon.com/durable-execution/">AWS Durable Execution SDK Developer Guide</a></li></ul><p><em>AWS is still changing this area quickly, so check the current SDK reference before using these snippets in production.</em></p><p>If this helped, give it a clap and follow for more practical breakdowns on cloud and AI.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=506f4f703821" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/aws-lambda-durable-functions-steps-checkpoints-replay-and-real-world-workflows-506f4f703821">AWS Lambda Durable Functions: Steps, Checkpoints, Replay, and Real-World Workflows</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The CNN Behind Our Vision Models: ResNet-152, Multi-Label, on SageMaker]]></title>
            <link>https://medium.com/awsfullstack/the-cnn-behind-our-vision-models-resnet-152-multi-label-on-sagemaker-2b1c50706655?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/2b1c50706655</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[convolutional-network]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[cnn]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Fri, 06 Nov 2026 13:11:01 GMT</pubDate>
            <atom:updated>2026-11-06T13:11:01.473Z</atom:updated>
            <content:encoded><![CDATA[<h4>What a CNN is, why we used ResNet-152, the exact training settings, and how its scores become answers in our app.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pBL0VUMud17cUe9u1ZNWMg.png" /></figure><p>Our system uses cameras to understand what’s in front of it: how many objects are in view, how close they are, and whether something is blocking the left side. Each photo can have <strong>several labels at once</strong>. To do that, we use a <strong>convolutional neural network (CNN)</strong> called <strong>ResNet-152</strong>, trained with SageMaker’s built-in image-classification algorithm.</p><p>This post explains how that model works, in plain terms first and then with our real settings. You don’t need a deep-learning background to follow it.</p><h3>What a CNN does</h3><p>A CNN looks at a photo through lots of small filters. Each filter slides across the image looking for one simple pattern.</p><ul><li><strong>Early layers</strong> find edges and color changes.</li><li><strong>Middle layers</strong> combine those into shapes and textures.</li><li><strong>Later layers</strong> combine shapes into parts of objects.</li><li><strong>The final layer</strong> turns all of that into one score per label.</li></ul><p>Nobody programs those filters by hand. The network learns them from labeled photos during training.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oJk3Cn4lLsQSKXNpqcHbbQ.png" /></figure><h3>Why ResNet-152</h3><p>More layers let a network learn more complex patterns. But very deep networks used to be hard to train: the learning signal got weaker as it passed back through dozens of layers.</p><p><strong>ResNet</strong> fixed this with <strong>shortcut connections</strong>. Each block of layers learns a small change and adds it to its input, and the input also skips straight past the block. That shortcut keeps the signal strong, so networks can be very deep. <strong>ResNet-152</strong> has 152 layers.</p><p>In our settings, that’s one line: num_layers: &quot;152&quot;.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VAUD-JXZsRH7b54RCZrzQg.png" /></figure><h3>We didn’t start from zero: transfer learning.</h3><p>Training 152 layers from scratch needs millions of photos. We have tens of thousands. So we use <strong>transfer learning</strong>:</p><ul><li><strong>use_pretrained_model: &quot;1&quot;</strong> starts from a ResNet already trained on ImageNet, a huge public photo dataset. It already knows edges, shapes, and textures.</li><li>Our training then teaches it <strong>our</strong> labels on <strong>our</strong> photos.</li></ul><p>We also often go one step further and start from <strong>our own previous model</strong> instead of ImageNet. That takes about <strong>10 passes</strong> over the data (epochs) instead of 50, because the model already knows most of the job.</p><h3>One photo, many labels</h3><p>Most image tutorials pick <strong>one</strong> label per photo: cat or dog. Ours need several at once, like “few objects,&quot; “close range,” and “blocked on the left.&quot;</p><ul><li><strong>multi_label: &quot;1&quot;</strong> tells the algorithm that each photo can have several labels.</li><li>The model returns <strong>a score between 0 and 1 for every label</strong>, in the same order as our label map.</li></ul><h3>Our exact training settings</h3><p>These are the defaults every model starts from (all values are strings, because SageMaker requires that):</p><pre>const BASE_HYPERPARAMETERS = {<br>  num_layers: &quot;152&quot;,           // ResNet-152<br>  image_shape: &quot;3,224,224&quot;,    // colour photos resized to 224 × 224<br>  use_pretrained_model: &quot;1&quot;,   // start from ImageNet weights<br>  multi_label: &quot;1&quot;,            // several labels per photo<br>  epochs: &quot;50&quot;,                // 10 when building on our previous model<br>  mini_batch_size: &quot;32&quot;,<br>  optimizer: &quot;sgd&quot;,<br>  learning_rate: &quot;0.001&quot;,<br>  momentum: &quot;0.9&quot;,<br>  weight_decay: &quot;0.0001&quot;,<br>  augmentation_type: &quot;crop_color_transform&quot;,<br>  use_weighted_loss: &quot;0&quot;,<br>  precision_dtype: &quot;float32&quot;,<br>  top_k: &quot;2&quot;,                  // must be 2 or more, or the job fails<br>  early_stopping: &quot;false&quot;,<br>  checkpoint_frequency: &quot;1&quot;,<br>};</pre><p>In plain English:</p><ul><li><strong>224 × 224 color photos.</strong> Every photo is resized to the size the network expects.</li><li><strong>32 photos per step.</strong> The model adjusts its filters after each batch of 32.</li><li><strong>Learning rate 0.001 with SGD and momentum.</strong> Small, steady steps. Momentum keeps it moving in a consistent direction.</li><li><strong>Weight decay.</strong> A small penalty that discourages the model from memorising.</li><li><strong>crop_color_transform.</strong> During training, photos are randomly cropped and their colours slightly changed, so the model copes with real-world variety like blur or poor lighting.</li><li><strong>use_weighted_loss: &quot;0&quot;.</strong> We deliberately don&#39;t let the model compensate for rare labels. Instead, we show the imbalance to our team, so they go and label more photos of the rare ones.</li><li><strong>No early stopping.</strong> Every run trains the full schedule, so runs are comparable.</li></ul><p>Two things are filled in automatically for every run: <strong>num_classes</strong> (how many labels the model has) and <strong>num_training_samples</strong>. Nobody can override them, because a wrong value produces a model that gives confident, wrong answers.</p><p>Training runs on one <strong>ml.g4dn.xlarge</strong>, a single NVIDIA T4 GPU.</p><h3>From scores to answers</h3><p>The model returns a list of scores. Our app turns them into answers with two simple rules.</p><p><strong>Rule 1: Some labels are a group, and only one can win.</strong> “Many objects” and “few objects” can’t both be true. So within a group, the highest score wins:</p><pre>for (const group of groups) {<br>  const present = group.filter((label) =&gt; probMap.has(label));<br>  if (present.length === 0) continue;<br>  let maxLabel = present[0], maxProb = -1;<br>  for (const label of present) {<br>    const p = probMap.get(label) ?? 0;<br>    if (p &gt; maxProb) { maxProb = p; maxLabel = label; }<br>  }<br>  const hasWinner = maxProb &gt; 0;<br>  for (const label of present) labels.set(label, hasWinner &amp;&amp; label === maxLabel);<br>}</pre><p><strong>Rule 2: other labels stand alone, with a 0.5 threshold.</strong> “Low lighting” is either true or not, so it’s true when its score is <strong>0.5 or more</strong>.</p><p>Two edge cases we learned the hard way:</p><ul><li><strong>If every score in a group is 0, nobody wins.</strong> That’s the model saying “none of these,&quot; not “the first one.&quot;</li><li><strong>A label the model doesn’t have shouldn’t read as &quot;no.&quot;</strong> An older model trained before a label existed returns no score for it. For those labels, the app shows “can’t tell” instead of quietly answering “no” without the model ever looking.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UysSgYefLFMkr1HPPvQPMQ.png" /></figure><h3>What we’d try next</h3><p>ResNet-152 is a big network. A smaller depth, such as ResNet-50, might answer faster and cost less to serve, possibly with similar accuracy on our labels. We haven’t tested that yet. With the training pipeline in place, it’s now a one-setting experiment.</p><h3>Checklist if you’re using the built-in image classifier</h3><ol><li><strong>One label or several per photo?</strong> Set multi_label to match.</li><li><strong>Do you have millions of photos?</strong> If not, keep use_pretrained_model on.</li><li><strong>Is </strong><strong>top_k 2 or more?</strong> A value of 1 fails the job.</li><li><strong>Do group labels use “highest score wins,&quot; and standalone labels a threshold?</strong></li><li><strong>Does a label the model doesn’t have to read as “can’t tell,&quot; not “no”?</strong></li></ol><h3>Final thoughts</h3><p>You don’t need to design a CNN to use one well. The built-in algorithm gave us a proven 152-layer network that already understood photos. Our work was choosing sensible settings and being careful about how its scores turn into answers in the app.</p><h3>References</h3><ul><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/image-classification.html">Image classification: MXNet (SageMaker built-in algorithm)</a></li><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/IC-Hyperparameter.html">Image classification hyperparameters</a></li><li><a href="https://arxiv.org/abs/1512.03385">Deep Residual Learning for Image Recognition (He et al., the ResNet paper)</a></li><li><a href="https://aws.amazon.com/ec2/instance-types/g4/">Amazon EC2 G4 instances</a></li></ul><p>If this helped, give it a clap and follow for more practical breakdowns on cloud and AI.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2b1c50706655" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/the-cnn-behind-our-vision-models-resnet-152-multi-label-on-sagemaker-2b1c50706655">The CNN Behind Our Vision Models: ResNet-152, Multi-Label, on SageMaker</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Moved Our Whole SageMaker Setup to Another AWS Region With One Small Change]]></title>
            <link>https://medium.com/awsfullstack/how-we-moved-our-whole-sagemaker-setup-to-another-aws-region-with-one-small-change-19aab59efffe?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/19aab59efffe</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Thu, 05 Nov 2026 15:16:24 GMT</pubDate>
            <atom:updated>2026-11-05T15:16:23.309Z</atom:updated>
            <content:encoded><![CDATA[<h4>The change was 16 lines. The checklist before it was much longer, and that’s the real lesson.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CoXwYY1aajDAzbEct3HNvQ.png" /></figure><p><em>Part 5 of 5 · SageMaker Without an ML Team. The full series is listed at the end.</em></p><p>Imagine moving house. You don’t carry the furniture out and hope the new place is ready. You set up the new house completely, check the lights and water work, and only then change your address. You keep the old house for a while too, so if something’s wrong you can go back.</p><p>That’s how we moved our whole <strong>AWS SageMaker</strong> setup- every model, training job, and photo- from the <strong>Frankfurt</strong> region to <strong>Stockholm</strong>. The actual switch was <strong>16 lines added to one file, nothing removed</strong>. Everything that made it safe happened before that.</p><h3>The trick: same names on both sides</h3><p>Before the switch, we had already created all <strong>19 models</strong> (SageMaker “endpoints”) in the new region, with <strong>the same names</strong> as the old ones.</p><p>That one choice made everything simpler. Our system looks up models by name. Same names meant nothing else had to change. The only question was which region to look in, and that became a single setting. Flip it, and everything points at the new region at once.</p><h3>The checklist before the switch</h3><p>The change was tiny. The list of things we checked first was not.</p><p><strong>Done in the weeks before:</strong></p><ul><li><strong>All 19 models were running in the new region,</strong> and each gave exactly the same answer as the old one for the same photo.</li><li><strong>All 65,967 photos were copied,</strong> and the counts matched.</li><li><strong>The training history was copied</strong> (18 records), so future training still knew what came before.</li><li><strong>The training setup was deployed</strong> in the new region.</li><li><strong>A real training job was run</strong> in the new region to prove it worked, instead of trusting a table.</li></ul><p><strong>Done in the last hour, because they go out of date:</strong></p><ul><li><strong>A final copy of new photos,</strong> since cameras kept uploading to the old region right up to the switch.</li><li><strong>Warming up all 19 models.</strong> A “cold” model takes about 18 seconds to answer the first time, against under a second when warm. Switching to 19 cold models at once would have looked like an outage.</li><li><strong>Checking that no training job was running.</strong> A job running during the switch would report back to the old region, where nobody was listening anymore, and look stuck forever.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RJFICYBUbeYbQJCXaZ4nGg.png" /></figure><h3>Why such a big switch was safe</h3><p>We <strong>deleted nothing</strong>. The old region kept all its models, photos, and setup running.</p><p>So if anything went wrong, undoing it was one step: <strong>revert the change</strong>, and everything pointed back at a setup that had been working ten minutes earlier.</p><p>A gradual rollout limits <strong>how much</strong> breaks at once. Keeping the old setup alive limits <strong>how long</strong> it stays broken. For a change you can undo instantly, the second matters more. The cost is paying for two of everything for a while, and resisting the urge to tidy up too early.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vIouz7UfNYT3iXOs1mQQIg.png" /></figure><h3>Copy only what you need first</h3><p>Copying everything, all the photos, was about <strong>440 GB</strong>: hours of work and roughly $9 in transfer costs. The models themselves only needed about <strong>3.3 GB</strong>, which copied in minutes for pennies.</p><p>The money wasn’t the point. Copying the small part first let us prove the models worked in the new region before moving the big part.</p><h3>It still broke, a little</h3><p>After the switch, every finished training job got stuck on a status called <strong>“Repacking”</strong>, forever, with no error anywhere.</p><p>The cause: our code reads the region from a setting, and if the setting is missing, it quietly falls back to the <strong>old</strong> region. One function in the new region was never given that setting. So it kept calling the old region, where it no longer had permission, and the job just stopped.</p><p>The fix was one line per function: tell every function which region it’s in.</p><p>The test we added is the clever part. It checks the setup in a region that is <strong>not</strong> the fallback one. If you test with the fallback region, the bug passes, because the wrong answer and the right answer look the same.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nsoogac3Jsy609UwCbfZFQ.png" /></figure><h3>Your checklist for a region move</h3><ol><li><strong>Can the new region run with the same names as the old one?</strong> Then the switch can be one setting.</li><li><strong>Have you proved it works there,</strong> with real answers and a real training job?</li><li><strong>Are the last-minute steps done in the last hour:</strong> final copy, warm-up, nothing running?</li><li><strong>Is the old region still running,</strong> so undoing is one step?</li><li><strong>Does every part of your system know which region it’s in,</strong> with no silent fallback?</li></ol><h3>Wrapping up the series</h3><p>Across all five posts, the expensive problems had one thing in common: <strong>none of them crashed loudly</strong>. A score that looked too good. A setting that was quietly ignored. A job stuck with no error. We only found them by checking.</p><p>So the most useful question isn’t “does it work?” It’s <strong>“if this were wrong, would I be able to tell?”</strong> Wherever the answer is no, that’s where to add a check.</p><p>Thanks for reading the series.</p><h3>References</h3><ul><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/serverless-endpoints.html">Amazon SageMaker Serverless Inference</a></li><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg-ecr-paths/sagemaker-algo-docker-registry-paths.html">Docker registry paths for built-in algorithms</a></li></ul><p>If this helped, give it a clap and follow for more practical breakdowns on cloud and AI.</p><h3>Read the series: SageMaker Without an ML Team</h3><ol><li><a href="https://medium.com/p/3719c22a9064">5 Mistakes That Wasted Our Money on AWS SageMaker</a></li><li><a href="https://medium.com/p/8863d50b704c">Your ML Experiment Just Became Everyone’s Default</a></li><li><a href="https://medium.com/p/df43de572607">Life After SageMaker Ground Truth</a></li><li><a href="https://medium.com/p/8f3c16a97de6">Your Train/Test Split Is Probably Leaking</a></li><li><strong>How We Moved Our Whole SageMaker Setup to Another AWS Region</strong> (you are here)</li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=19aab59efffe" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/how-we-moved-our-whole-sagemaker-setup-to-another-aws-region-with-one-small-change-19aab59efffe">How We Moved Our Whole SageMaker Setup to Another AWS Region With One Small Change</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[“API Key Not Found” on Sandbox Redeploy: Using Sandbox Identifiers]]></title>
            <link>https://medium.com/awsfullstack/api-key-not-found-on-sandbox-redeploy-using-sandbox-identifiers-0ab356068a73?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/0ab356068a73</guid>
            <category><![CDATA[aws-amplify]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[aws-appsync]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Poorna Theekshana]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 14:01:06 GMT</pubDate>
            <atom:updated>2026-11-04T14:01:04.242Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YppN-Lzt60-AMI3QEub0ww.png" /></figure><p>Starting a second Amplify Gen 2 project usually takes a few minutes. You clone the same Next.js starter you used for the first one, install the dependencies, and run npx ampx sandbox in the new folder.</p><p>That first deploy fails. When you look closer, the command targeted the sandbox of your other project. A quick search suggests giving the new sandbox its own name, so you run npx ampx sandbox --identifier blog, and this time it deploys cleanly.</p><p>The next morning you open the first project, start its sandbox as usual, and the deploy stops a few seconds in:</p><pre>[ERROR] [UnknownFault] ToolkitError: ❌  amplify-awsamplifygen2-alex-sandbox-a8968da97f failed: NotFoundException: API key not found: da2-...</pre><p>Nothing changed in that project. Its code is the same as yesterday, and its sandbox was healthy when you closed the terminal. The trouble comes from an assumption most of us make without noticing: that a sandbox belongs to the folder you run it from.</p><p>In this article, we will look at how two projects end up sharing one sandbox, how to recover from the ampx sandbox api key not found error, and how to prevent it with sandbox identifiers.</p><h3>Where the Sandbox Name Comes From</h3><p>The <a href="https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/setup/">sandbox setup docs</a> say a deploy creates a CloudFormation stack named amplify-&lt;app-name&gt;-&lt;$(whoami)&gt;-sandbox. The <a href="https://github.com/aws-amplify/amplify-backend/blob/main/packages/cli/src/commands/sandbox/sandbox_id_resolver.ts">ampx source</a> fills in the details. The app name is the name field of the package.json in the directory where you run the command. The second part is your system username, or the value of --identifier when you pass one. A short hash of those two values goes on the end.</p><p>The folder path is not one of the inputs. Two folders with the same package.json name, run by the same user, resolve to the same stack.</p><p>Starter templates make this easy to hit. The <a href="https://github.com/aws-samples/amplify-next-template">Next.js starter</a> ships with &quot;name&quot;: &quot;aws-amplify-gen2&quot; in its package.json, and many projects never change that line. Running the naming logic from @aws-amplify/backend-cli 1.10.0 for a user called alex gives these stack names:</p><pre>~/code/shop                      amplify-awsamplifygen2-alex-sandbox-a8968da97f<br>~/code/blog                      amplify-awsamplifygen2-alex-sandbox-a8968da97f<br>~/code/blog --identifier blog    amplify-awsamplifygen2-blog-sandbox-8b27e47144<br>~/code/blog, name &quot;blog-app&quot;     amplify-blogapp-alex-sandbox-11c16b5c72</pre><p>The first two lines are the same stack. The last two are separate stacks, which is what you expected to get from the start.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gMJvDPfvAi0xgvgzsbfPAA.png" /></figure><h3>How a Shared Name Turns Into “API Key Not Found”</h3><p>This exact sequence is recorded in <a href="https://github.com/aws-amplify/amplify-backend/issues/3044">amplify-backend issue #3044</a>. The reporter started a new project, ran npx ampx sandbox, and got an error. They then realized the command was deploying to the existing project&#39;s sandbox, moved the new project to --identifier project2, and that deploy worked. Back in the original project, every redeploy failed with NotFoundException: API key not found.</p><p>A maintainer traced the failure to the AppSync API key. According to that investigation, “CloudFormation still has a reference to the old API key ID”, tries to update it, and AppSync replies that the key does not exist. The log in the issue shows UpdateApiKeyCommand as the last AWS call before the deploy failed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HzcQ94_EN_myx5GKy4KvlA.png" /></figure><p>The issue is still open, labeled as a bug at priority p2, and no fix or safe in-place repair has been published. Until then, the practical step is to stop two projects from sharing a stack, which is the part you can control.</p><h3>Confirm It Before Deleting Anything</h3><p>Before you remove anything, check that this Amplify sandbox redeploy error really comes from a shared name:</p><ol><li>Read the stack name in the error line. It starts with amplify-, then the package name with punctuation removed, then the sandbox name.</li><li>Open package.json in each of your Amplify projects. If two of them have the same name and you ran both without --identifier, they shared a stack.</li><li>Open <a href="https://us-east-1.console.aws.amazon.com/amplify/apps/sandboxes">Manage sandboxes</a> in the Amplify console, which lists the sandboxes in your account, their status, and last update.</li></ol><p>If each project has a different name, the shared stack isn&#39;t the cause here. The issue thread is then a better place to compare your logs with the ones already reported.</p><h3>Recovering the Sandbox</h3><p>Clearing local build files is the usual first step. In the issue, rm -rf cdk.out .amplify did not help. The mismatch is in the deployed stack, so files on your machine are not where they can be repaired.</p><p>Deleting the CloudFormation stack from the AWS console is also tempting. The maintainer withdrew that suggestion in the same thread and advised against it without a full backup, because removing the stack can remove the data in your sandbox tables and buckets.</p><p>The route the Amplify team recommends is to delete the sandbox with ampx and create it again. Run this from the project whose sandbox is broken, before you rename anything:</p><pre>npx ampx sandbox delete<br>npx ampx sandbox</pre><p>If the broken sandbox was started with a name, pass the same name to the delete command, for example npx ampx sandbox delete --identifier blog.</p><p>This removes the data in that sandbox. The team is direct about it in the thread: sandboxes are meant to be ephemeral, and the <a href="https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/setup/">setup docs</a> already describe cases where Amplify drops and recreates tables or user pools during normal sandbox work. If you need test data in the new sandbox, the <a href="https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/seed/">seed feature</a> lets you script it and load it with npx ampx sandbox seed. The reporter resolved the issue the same way, by deleting the sandbox and creating a new one.</p><h3>Preventing It: One Name per Project, One Identifier per Sandbox</h3><p>The lasting fix is a unique name in every project&#39;s package.json, set before its first sandbox deploy. For the second project in this example, change &quot;name&quot;: &quot;aws-amplify-gen2&quot; to &quot;name&quot;: &quot;blog-app&quot;.</p><p>Changing the name changes the stack name, so Amplify treats the project as a new sandbox. The old stack stays in your account until you delete it, which is why the recovery step comes first.</p><p>When you want more than one sandbox for the same project, use an ampx sandbox identifier:</p><pre>npx ampx sandbox --identifier feature-auth</pre><p>A few details matter once you start using identifiers:</p><ul><li>The CLI accepts letters, numbers, and hyphens, up to 15 characters.</li><li>Secrets are stored per sandbox. According to the <a href="https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/features/">sandbox features docs</a>, a named sandbox needs its own values, set with npx ampx sandbox --identifier feature-auth secret set &lt;name&gt;.</li><li>You can switch between named sandboxes, but only one can run at a time.</li><li>Each deploy writes amplify_outputs.json to the project root, so your frontend talks to whichever sandbox deployed last.</li></ul><p>The same docs recommend one sandbox per developer per app, since multiple sandboxes make it harder to track which code is deployed where.</p><h3>Final Thoughts</h3><p>The API key in the error message is where the deploy failed. The cause happened earlier, when two projects resolved to the same sandbox name. Amplify builds that name from the package.json name and your username, and the folder you run the command in plays no part.</p><p>If you only ever work on one Amplify project, the default name is fine. The check matters on the day you start a second project from the same template, and it takes one line in package.json.</p><h3>FAQ</h3><h4>What does “NotFoundException: API key not found” mean on a sandbox redeploy?</h4><p>When you see the ampx sandbox api key not found error, CloudFormation is trying to update an AppSync API key that no longer exists. In the reported case, it followed two projects deploying to the same sandbox stack.</p><h4>Will npx ampx sandbox delete remove my data?</h4><p>Yes. It deletes the resources in that sandbox, including its tables. Use the seed feature if you need test data again.</p><h4>What is a sandbox identifier?</h4><p>A name you pass with --identifier. It replaces your username in the sandbox stack name, so one project can have several separate sandboxes. It accepts letters, numbers, and hyphens, up to 15 characters.</p><p>Next in this series: sandbox hot reload failing with an “already exists” error, and how to recover without starting over.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0ab356068a73" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/api-key-not-found-on-sandbox-redeploy-using-sandbox-identifiers-0ab356068a73">“API Key Not Found” on Sandbox Redeploy: Using Sandbox Identifiers</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Your Train/Test Split Is Probably Leaking (and Why a Good Score Can Be Lying)]]></title>
            <link>https://medium.com/awsfullstack/your-train-test-split-is-probably-leaking-and-why-a-good-score-can-be-lying-8f3c16a97de6?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/8f3c16a97de6</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 13:01:03 GMT</pubDate>
            <atom:updated>2026-11-04T13:01:03.462Z</atom:updated>
            <content:encoded><![CDATA[<h4>Adding ten photos quietly moved most of our test photos into training. The model’s score went up, and that’s exactly why nobody noticed.</h4><p><em>Part 4 of 5 · SageMaker Without an ML Team. The full series is listed at the end.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wKvDt9y5vRlnOe1--UNXCw.png" /></figure><p>Imagine a teacher who sets a final exam, but some of the questions are copied straight from the homework. A student who memorized the homework gets a great score. It looks like they learned the subject. They didn’t. They just remembered the answers.</p><p>That’s what happened to one of our image models on <strong>AWS SageMaker</strong>. The score came back <strong>better than expected</strong>. Nobody investigates good news, so nobody looked. It turned out the model was being tested on photos it had already trained on.</p><h3>Quick background: train and test photos</h3><p>When you train a model, you split your photos into two groups:</p><ul><li><strong>Training photos:</strong> the model learns from these.</li><li><strong>Test photos</strong> (also called validation): kept aside to check how well the model does on photos it has <strong>never seen</strong>.</li></ul><p>If test photos leak into training, the score stops measuring learning and starts measuring memory. It goes up, and it’s wrong.</p><h3>How we split our photos</h3><p>We shuffled the list of photos in a fixed, repeatable way (using a “seed”, so the same list always shuffles the same way), then took the first 80% for training and the rest for testing.</p><p>Same list in, same split out every time. It looked perfectly reproducible.</p><h3>What went wrong?</h3><p>The shuffle is only repeatable if the list is <strong>exactly the same</strong>. Add even one photo, and the whole shuffle comes out different, because the length of the list affects every step of the shuffle.</p><p>When we added <strong>ten new photos</strong>, we measured what happened: about <strong>80% of the old test photos moved into the training group</strong>. Same seed, same code. A completely different split.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ayp4BLQQbkufVIIADZHsIg.png" /></figure><h3>Why that was a big problem for us</h3><p>If you train every model from scratch, a changing split is just untidy. Each model starts fresh, so it hasn’t seen any photos before.</p><p>But to save time, we often trained a <strong>new model on top of the previous one</strong>. The previous model had already learned from most of our photos. When the split reshuffled, many of the new model’s test photos were ones the previous model had trained on.</p><p>So the new model was taking an exam with the homework questions on it. Its score looked great. And each new model was then compared against that inflated score, so the problem carried forward.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nV39ggVn16cEj2-1v2zSGQ.png" /></figure><h3>The 4-line fix</h3><p>Instead of shuffling a list, we let <strong>each photo decide its own group based</strong> on its filename.</p><p>We turn the filename into a number (a “hash”), then use that number to place the photo:</p><pre>function isTrainSide(filename: string, ratio: number, seed: number): boolean {<br>  return fnv1a(filename, seed) % 10000 &lt; ratio * 100;<br>}</pre><p>In plain English: every photo gets a fixed number between 0 and 9,999. If you want 80% for training, photos numbered below 8,000 go to training, and the rest go to testing.</p><p>Now adding new photos <strong>doesn’t move any existing photos</strong>. Each photo’s group depends only on its own name.</p><p>The small trade-off: you get roughly 80%, not exactly 80% (say 80.05%). So before training starts, we check that neither group came out empty.</p><h3>A twist we didn’t expect</h3><p>Because each photo has a fixed number, changing the training percentage only works safely in one direction:</p><ul><li><strong>Raising it</strong> (80% → 90%) is safe. The photos still held out for testing were also held out before.</li><li><strong>Lowering it</strong> (90% → 80%) brings the leak back. Photos numbered 8,000 to 8,999 were training photos for the previous model, and now they’d be test photos.</li></ul><p>So when you build on an earlier model, our system now <strong>refuses to lower the training percentage</strong> and explains why.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Su62Cnf0wGNKq7Jr7dUQCQ.png" /></figure><h3>What’s still not fixed</h3><p>Our cameras take photos a few seconds apart, so two photos can be almost identical. If one lands in training and its near-twin lands in testing, that’s still a small leak. Grouping photos by the moment they were taken, instead of by filename, would fix it. We haven’t done that yet.</p><h3>Your checklist for a split that doesn’t leak</h3><ol><li><strong>Does adding new photos move existing ones between groups?</strong> If so, split by a hash of each photo, not a shuffle.</li><li><strong>Do you train new models on top of old ones?</strong> Then a leaky split will make your scores look too good.</li><li><strong>Do you lower the training percentage between runs?</strong> Don’t, if you’re building on an earlier model.</li><li><strong>Are near-identical photos kept in the same group?</strong></li><li><strong>Do you double-check a score that’s better than expected?</strong></li></ol><h3>Final thoughts</h3><p>A mistake that makes your score look worse gets noticed the same day. A mistake that makes it look better can hide for weeks, because everyone is happy with the result. When a model suddenly scores higher than it should, that’s the time to check your split.</p><p>Next in this series: how we moved our whole SageMaker setup to another AWS region with one small change.</p><h3>References</h3><ul><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/image-classification.html">Train and validation data for SageMaker image classification</a></li><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/incremental-training.html">Incremental training in Amazon SageMaker</a></li></ul><p>If this helped, give it a clap and follow for more practical breakdowns on cloud and AI.</p><h3>Read the series: SageMaker Without an ML Team</h3><ol><li><a href="https://medium.com/p/3719c22a9064">5 Mistakes That Wasted Our Money on AWS SageMaker</a></li><li><a href="https://medium.com/p/8863d50b704c">Your ML Experiment Just Became Everyone’s Default</a></li><li><a href="https://medium.com/p/df43de572607">Life After SageMaker Ground Truth</a></li><li><strong>Your Train/Test Split Is Probably Leaking</strong> (you are here)</li><li><a href="https://medium.com/p/19aab59efffe">How We Moved Our Whole SageMaker Setup to Another AWS Region</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8f3c16a97de6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/your-train-test-split-is-probably-leaking-and-why-a-good-score-can-be-lying-8f3c16a97de6">Your Train/Test Split Is Probably Leaking (and Why a Good Score Can Be Lying)</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Life After SageMaker Ground Truth: How We Replaced AWS Labeling With One File and Our Own Tool]]></title>
            <link>https://medium.com/awsfullstack/life-after-sagemaker-ground-truth-how-we-replaced-aws-labeling-with-one-file-and-our-own-tool-df43de572607?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/df43de572607</guid>
            <category><![CDATA[sagemaker]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[data-labeling]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 14:37:38 GMT</pubDate>
            <atom:updated>2026-11-03T14:37:36.095Z</atom:updated>
            <content:encoded><![CDATA[<h4>AWS shut down its labeling service in 2026. We had left months earlier. Here’s why, and what we use now.</h4><p><em>Part 3 of 5 · SageMaker Without an ML Team. The full series is listed at the end.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mzjOlcnOaQG0Ku7ynYslsg.png" /></figure><p>Imagine hiring an events company to hand out name tags at a dinner for five friends. They’d do a great job, but you’d be paying for staff, scheduling, and coordination you never needed. You could just write the name tags yourself.</p><p>That’s how <strong>SageMaker Ground Truth</strong>, AWS’s service for labeling training images, felt for our small team. We stopped using it early in 2026. Then AWS closed it to new customers on <strong>30 July 2026</strong> and shut it down completely on <strong>30 September 2026</strong>, alongside Mechanical Turk. Here’s why it didn’t fit us and the much simpler setup that replaced it.</p><h3>What Ground Truth does</h3><p>To train an image model, someone has to look at each photo and say what’s in it. That’s labeling. Ground Truth helped you manage that work: it found or organised the people doing the labeling, sent them photos, checked their quality, and combined their answers when several people labeled the same photo.</p><p>That’s genuinely useful if you need <strong>hundreds of thousands of photos</strong> labeled once by people you don’t employ.</p><h3>Why it didn’t fit us</h3><p>Our situation was the opposite:</p><ul><li><strong>Our labelers were our own colleagues.</strong> No outside workforce to manage.</li><li><strong>Nobody labeled the same photo twice,</strong> so there were no answers to combine.</li><li><strong>New photos arrived every day in small batches.</strong> The real job was “label last night’s 40 photos,&quot; a five-minute task, not a big labeling project.</li></ul><p>Ground Truth is built around big, one-off <strong>labeling jobs</strong>. Every batch meant setting up a new job, waiting, and checking it the next day. And you <strong>can’t edit a job</strong>, only copy it. When someone mistyped a job name, the only fix was to stop it and start a new copy. The typo stayed in the console forever.</p><p>Our console ended up with <strong>seven jobs that produced two usable datasets</strong>. The per-photo fee was small. The real cost was all the setting up, waiting, and redoing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TdFb5SlESsR1t5DYD-oFlQ.png" /></figure><h3>The surprise: training only needs one file.</h3><p>When we looked at what Ground Truth actually produced, it was just a <strong>text file</strong>. One line per photo, saying where the photo is stored and which labels it has:</p><pre>{&quot;source-ref&quot;: &quot;s3://my-bucket/images/img_0417.jpg&quot;, &quot;my-labels&quot;: [0, 3, 7]}</pre><p>SageMaker’s training never talks to Ground Truth. It only reads this file. (The real file has a few more housekeeping fields, which the training ignores.) So once you have your own labelers, you don’t need the service. <strong>You just need to produce the file.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kX_7sqDsCO1t-akeHNWDBQ.png" /></figure><h3>What we built instead</h3><p>The file is only one piece. Replacing ground truth took four:</p><ul><li><strong>A labeling screen</strong> where our team opens a photo, ticks labels, and saves. The biggest piece.</li><li><strong>A file writer</strong> that turns saved labels into that one file. About an afternoon of work.</li><li><strong>A train/test split</strong> that decides which photos are for training and which are for testing. Harder than it looks (that’s a later post).</li><li><strong>A missing-photo check</strong> before training starts, so one deleted photo doesn’t crash the job (see Part 1).</li></ul><h3>Our labeling tool, in four screens</h3><p>The tool follows the life of a photo: <strong>Unlabeled → Labeled → Train → Stats</strong>. A new photo arrives with no labels, someone labels it; it’s used in training; and the results show up in the stats. Every screen stays clickable, so you can check the stats first and then go label whatever’s missing.</p><h3>AI suggestions, with two rules</h3><p>We added AI suggestions that propose labels for a photo. To keep the data trustworthy, two rules are written at the top of that part of the code:</p><ul><li><strong>The AI only runs when you ask it to.</strong> Opening the screen costs nothing.</li><li><strong>Nothing is saved without a person clicking Accept.</strong></li></ul><p>There is an “Accept All” button, so a tired person could accept 50 suggestions without looking closely. To catch that, every accepted label records one of two values: <strong>accepted as suggested or</strong> <strong>edited before accepting</strong>.</p><p>That one small detail tells us how good the AI really is. If people edit a lot of suggestions, the AI isn’t good enough yet. If someone accepts hundreds with zero edits, they probably weren’t looking.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9N4gCmtvXfuxixfKciI8aA.png" /></figure><h3>What we gave up</h3><p>Our old, rough labeling tool had <strong>keyboard shortcuts</strong>: number keys for labels and Enter to save and move on. The new tool is mouse-only. It’s much better in almost every way but slower per photo. We chose trustworthy data over speed per photo, and keyboard shortcuts are on the list.</p><h3>If you’re replacing a labeling service</h3><ol><li><strong>Do you employ your labelers?</strong> If yes, you probably don’t need a workforce service.</li><li><strong>What file does your training actually read?</strong> Produce that file yourself.</li><li><strong>Does your labeling tool make it easy to label small batches every day?</strong></li><li><strong>If you use AI suggestions, does a person always approve them?</strong></li><li><strong>Do you record whether the AI’s suggestion was edited?</strong> That’s how you’ll know if it’s any good.</li></ol><h3>Final thoughts</h3><p>Ground Truth was a good product for big, one-off labeling projects with outside workers. For a small team labeling a few photos every day, it was mostly overhead. The training only ever needed one file, and producing that file ourselves turned out to be the easy part.</p><p>Next in this series: why your train/test split might be quietly leaking, and how that makes a model look better than it is.</p><h3>References</h3><ul><li><a href="https://encord.com/blog/amazon-mturk-sagemaker-shutdown-alternative/">Amazon is shutting down Mechanical Turk and SageMaker Ground Truth (Encord).</a></li><li><a href="https://letsdatascience.com/news/amazon-closes-mechanical-turk-crowdsourcing-platform-septemb-f1003080">Amazon closes Mechanical Turk on September 30 (Let’s Data Science)</a></li><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/augmented-manifest.html">Augmented manifest files for SageMaker training</a></li></ul><p>If this helped, give it a clap and follow for more practical breakdowns on cloud and AI.</p><h3>Read the series: SageMaker Without an ML Team</h3><ol><li><a href="https://medium.com/p/3719c22a9064">5 Mistakes That Wasted Our Money on AWS SageMaker</a></li><li><a href="https://medium.com/p/8863d50b704c">Your ML Experiment Just Became Everyone’s Default</a></li><li><strong>Life After SageMaker Ground Truth</strong> (you are here)</li><li><a href="https://medium.com/p/8f3c16a97de6">Your Train/Test Split Is Probably Leaking</a></li><li><a href="https://medium.com/p/19aab59efffe">How We Moved Our Whole SageMaker Setup to Another AWS Region</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=df43de572607" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/life-after-sagemaker-ground-truth-how-we-replaced-aws-labeling-with-one-file-and-our-own-tool-df43de572607">Life After SageMaker Ground Truth: How We Replaced AWS Labeling With One File and Our Own Tool</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Fix the AWS Lambda 4 KB Environment Variable Limit]]></title>
            <link>https://medium.com/awsfullstack/how-to-fix-the-aws-lambda-4-kb-environment-variable-limit-314c2344d918?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/314c2344d918</guid>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[aws-systems-manager]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Pathum Jayaweera]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 13:01:05 GMT</pubDate>
            <atom:updated>2026-11-03T13:01:04.062Z</atom:updated>
            <content:encoded><![CDATA[<h4><strong>A practical guide to AWS Systems Manager Parameter Store, Standard and Advanced parameters, SecureString, KMS, permissions, caching, and cost</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2NrSplg2wKbwYc-r-ZaseA.png" /></figure><p>AWS Lambda environment variables are a convenient way to keep configuration outside your application code. But as a function grows and depends on more services, those values can eventually hit Lambda’s 4 KB total environment-variable limit.</p><p>When that happens, the better solution is usually not to keep removing or shortening variables. It is to reconsider where that configuration should live.</p><p>One practical option is AWS Systems Manager Parameter Store.</p><h3>Why the 4 KB Limit Becomes a Configuration Problem</h3><p>A Lambda function may start with only a few environment variables, but the list often grows as the application becomes more complex. It may eventually need database names, API endpoints, S3 bucket names, queue URLs, service configuration, feature settings, and environment-specific values.</p><p>For example:</p><pre>DATABASE_NAME<br>API_BASE_URL<br>BUCKET_NAME<br>QUEUE_URL<br>SERVICE_URL<br>SERVICE_KEY<br>LOG_LEVEL<br>OTHER_CONFIGURATION</pre><p>The important detail is that Lambda does not provide 4 KB for each variable. The combined size of all environment-variable names and values must fit within 4 KB.</p><p>This means a function can work normally for a long time and then suddenly fail during deployment after only one or two new values are added. The application code may be completely valid; the problem is simply that the function configuration has grown beyond what Lambda allows.</p><p>Removing an unused variable or shortening a value might temporarily make the deployment work again, but that does not really solve the problem. If the application continues to grow, the same limit is likely to appear again.</p><p>At that point, the issue is no longer just about fitting another value into Lambda. It becomes a configuration design question: does every configuration value really need to live directly inside the function?</p><p>Small values that are closely tied to the function can still remain as environment variables. But larger values, shared configuration, or settings that need to be managed centrally can be moved outside Lambda.</p><p>This is where AWS Systems Manager Parameter Store becomes useful. Instead of storing every full value inside the Lambda environment, the function can keep only a small reference and retrieve the actual configuration when it needs it.</p><h3>Where AWS Systems Manager Fits</h3><p>AWS Systems Manager is a broader AWS operations service, not just a configuration store. It includes several tools for managing AWS and hybrid environments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7uwZkOIco3gWjPDN3Jt7zw.png" /></figure><p>For example, Session Manager can provide controlled access to managed machines without relying on traditional SSH connections. Run Command can remotely execute commands, Patch Manager helps manage patching, State Manager helps keep systems in a desired configuration, and Automation can run operational workflows.</p><p>For the Lambda environment-variable problem, the relevant part is Parameter Store. Parameter Store provides a central place to keep configuration values such as service endpoints, resource identifiers, environment settings, and other application parameters. Instead of copying every full value into the Lambda environment, the function can retrieve the values it needs from Parameter Store at runtime.</p><p>A Lambda environment could therefore contain only a small configuration reference such as:</p><pre>CONFIG_PATH=/myapp/prod</pre><p>while the actual values live in Parameter Store:</p><pre>/myapp/prod/service-url<br>/myapp/prod/service-key</pre><p>This reduces pressure on the Lambda environment-variable limit and also gives the configuration a more organized structure.</p><h3>Moving Configuration from Lambda to Parameter Store</h3><p>Suppose an application needs two values. One is a normal service URL, while the other contains sensitive configuration. Parameter Store allows them to be handled differently:</p><pre>/myapp/prod/service-url<br>/myapp/prod/service-key</pre><p>The service URL can be stored as a normal String, while the sensitive value can be stored as a SecureString.</p><p>The same path structure can also be used to separate environments:</p><pre>/myapp/dev/service-url<br>/myapp/staging/service-url<br>/myapp/prod/service-url</pre><p>This makes configuration easier to manage because development, staging, and production values don&#39;t need to be mixed together. More importantly, the Lambda no longer needs to hold every full value in its own environment. It only needs enough information to locate the correct parameters and permission to retrieve them.</p><h3>Understanding Parameter Types and Parameter Tiers</h3><p>One of the easiest things to confuse in Parameter Store is the difference between a parameter type and a parameter tier.</p><p>The parameter type describes how the value is stored. Parameter Store supports String, StringList, and SecureString. A normal String is suitable for ordinary configuration such as an endpoint URL, while SecureString is used when a value needs encryption with AWS KMS.</p><p>The parameter tier is a separate decision. The main tiers are Standard and Advanced, and they affect the size limits, available features, quotas, and cost.</p><p>This means these combinations are all possible:</p><ul><li>Standard String</li><li>Standard SecureString</li><li>Advanced String</li><li>Advanced SecureString</li></ul><p>The key point is that SecureString does not mean Advanced. A sensitive value can still be stored as a Standard SecureString.</p><p>For many Lambda configuration use cases, Standard parameters are enough. A Standard parameter supports up to 4 KB per parameter, while Lambda provides 4 KB for the entire collection of environment variables.</p><p>Advanced parameters increase the maximum size to 8 KB per parameter and provide additional features such as parameter policies and larger parameter quotas. They also have additional charges.</p><p>For a Lambda that only needs to move a few configuration values out of its environment, Standard parameters are often the simpler choice.</p><h3>How SecureString and AWS KMS Work Together</h3><p>A normal String parameter suits configuration that doesn&#39;t require additional protection. For sensitive values, Parameter Store provides SecureString, which works with AWS Key Management Service (KMS).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OAEWtUko4dDynAtUtYSbUA.png" /></figure><p>The two services have different roles. Parameter Store manages the parameter, while KMS performs the encryption and decryption operations used to protect SecureString values.</p><p>When Parameter Store creates a Standard SecureString, it uses a symmetric KMS key to encrypt the value before storing it. Later, when an authorized application requests the parameter with decryption enabled, Parameter Store works with KMS to decrypt it before returning the value.</p><p>A customer managed KMS key can also be used when stronger access control is required. This makes it possible to control more precisely which applications, users, or IAM roles are allowed to decrypt values protected by that key.</p><p>The choice depends on the security requirements. The AWS managed key is convenient for straightforward use cases, while a customer managed key provides greater control when different applications need clearly separated decryption permissions.</p><h3>Lambda Still Needs IAM Permission</h3><p>Moving configuration to Parameter Store does not automatically give a Lambda function access to it. The function uses its IAM execution role when requesting the parameters, so the role needs the required permissions.</p><p>For example, the Lambda may need:ssm:GetParameter or ssm:GetParameters . If a SecureString uses a customer managed KMS key, the execution role may also need:kms:Decrypt</p><p>Keep these permissions as narrow as possible. A production function, for example, could be allowed to read only parameters under:/myapp/prod/* , while a staging function could be limited to:/myapp/staging/* . This provides a cleaner security boundary than giving every Lambda permission to read every parameter in the account.</p><h3>Retrieving Parameters Without Adding Unnecessary Latency</h3><p>Lambda can retrieve Parameter Store values through the AWS SDK. A simplified Node.js example looks like this:</p><pre>import {<br>  SSMClient,<br>  GetParametersCommand,<br>} from &quot;@aws-sdk/client-ssm&quot;;<br><br><br>const ssm = new SSMClient({});<br>async function loadConfig() {<br>  const response = await ssm.send(<br>    new GetParametersCommand({<br>      Names: [<br>        &quot;/myapp/prod/service-url&quot;,<br>        &quot;/myapp/prod/service-key&quot;,<br>      ],<br>      WithDecryption: true,<br>    })<br>  );<br>  const config = {};<br>  for (const parameter of response.Parameters ?? []) {<br>    config[parameter.Name] = parameter.Value;<br>  }<br>  return config;<br>}</pre><p>The WithDecryption: true option tells Parameter Store to return decrypted values for SecureString parameters when the caller has the required permissions.</p><p>One important difference exists between environment variables and Parameter Store. Reading an environment variable happens locally inside the Lambda runtime, while retrieving a Parameter Store value normally involves an AWS API request.</p><p>For configuration that rarely changes, making that request on every Lambda invocation is unnecessary. The values can often be loaded once and cached in memory while the Lambda execution environment remains warm.</p><pre>let cachedConfig;<br><br>export const handler = async (event) =&gt; {<br>  if (!cachedConfig) {<br>    cachedConfig = await loadConfig();<br>  }<br>  // Use cachedConfig<br>};</pre><p>AWS also provides the Parameters and Secrets Lambda Extension, which can retrieve and cache Parameter Store and Secrets Manager values locally. For small functions, simple in-memory caching may be enough, while the extension becomes more useful when configuration access becomes more frequent or complex.</p><h3>Cost and When Secrets Manager Makes More Sense</h3><p>For this Lambda configuration problem, Standard Parameter Store is usually the cheapest option. Standard parameters have no additional storage charge, and applications using the default standard throughput can access them without additional Parameter Store API charges.</p><p>There is an important difference between Lambda’s 4 KB limit and Parameter Store’s 4 KB limit. In Lambda, the combined size of all environment-variable names and values must fit within 4 KB. With Standard Parameter Store, each individual parameter can contain a value up to 4 KB.</p><p>For example, these can all be separate Standard parameters:</p><pre>/app/service-url      → up to 4 KB<br>/app/database-name    → up to 4 KB<br>/app/service-key      → up to 4 KB</pre><p>These parameters do not share one 4 KB space. Each parameter has its own maximum value size. This is why moving larger configuration values out of Lambda can help: instead of forcing all configuration into Lambda’s single 4 KB environment-variable limit, the values can be stored separately in Parameter Store.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-FFgq86JPjTb2I4yK8zNJQ.png" /></figure><p>Suppose a Lambda moves two values into Parameter Store: a service URL stored as a String and a sensitive value stored as a SecureString. If both use the Standard tier and the application stays within normal throughput, there is no additional Parameter Store storage charge or standard-throughput API charge. For a small configuration like this, the Parameter Store cost itself can effectively be $0.</p><p>Advanced parameters are different. They support values up to 8 KB per parameter, compared with 4 KB for Standard parameters, and also provide features such as parameter policies, higher parameter quotas, and cross-account sharing. Advanced parameters have additional charges, so they are more useful when those extra capabilities are actually required rather than simply because a value needs encryption.</p><p>SecureString introduces another part of the cost because it uses AWS KMS for encryption and decryption. When Parameter Store uses the AWS managed AWS/SSM KMS key, there is no monthly storage charge for that AWS managed key. KMS request charges can still apply depending on usage, and AWS currently provides a free tier for eligible KMS requests. A customer managed KMS key, on the other hand, introduces a key charge as well as applicable request charges.</p><p>This is also where AWS Secrets Manager becomes worth considering. Parameter Store is well suited to application configuration and relatively static encrypted values, while Secrets Manager is designed more specifically for credentials such as database passwords, API keys, OAuth tokens, and other secrets that may need features such as automatic rotation and stronger secret lifecycle management. AWS charges Secrets Manager based on the number of secrets stored and the API requests made to the service.</p><p>For the Lambda 4 KB problem discussed here, Standard Parameter Store is usually enough when the goal is simply to move growing application configuration outside the Lambda environment. Advanced Parameter Store becomes useful when larger values or additional Parameter Store features are required, while Secrets Manager is generally the better choice when the value is a real credential that needs automatic rotation or more dedicated secret management.</p><h3>The Better Way to Think About the 4 KB Limit</h3><p>The Lambda 4 KB environment-variable limit may look like a simple quota issue, but it can also signal that application configuration is outgrowing the function itself.</p><p>Environment variables are still useful for small, function-specific values. But as configuration grows, AWS Systems Manager Parameter Store provides a cleaner way to manage those values outside Lambda.</p><p>For most cases, String works for normal configuration, SecureString KMS can protect sensitive values, and Standard parameters are usually enough unless you need advanced features. IAM controls which functions can retrieve the parameters, while caching helps avoid unnecessary requests.</p><p>The goal is not to replace environment variables completely. It is to use them where they make sense and move growing configuration to a service designed to manage it.</p><h3>Connect with Me</h3><p>LinkedIn:<a href="https://www.linkedin.com/in/pathumscj"> https://www.linkedin.com/in/pathumscj</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=314c2344d918" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/how-to-fix-the-aws-lambda-4-kb-environment-variable-limit-314c2344d918">How to Fix the AWS Lambda 4 KB Environment Variable Limit</a> was originally published in <a href="https://medium.com/awsfullstack">AWS Fullstack</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>