<?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>Thu, 05 Nov 2026 18:21:05 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[How We Moved Our Whole SageMaker Setup to Another AWS Region With One Small Change]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/awsfullstack/how-we-moved-our-whole-sagemaker-setup-to-another-aws-region-with-one-small-change-19aab59efffe?source=rss----a02b1eedcccf---4"><img src="https://cdn-images-1.medium.com/max/2600/1*CoXwYY1aajDAzbEct3HNvQ.png" width="2800"></a></p><p class="medium-feed-snippet">The change was 16 lines. The checklist before it was much longer, and that&#x2019;s the real lesson.</p><p class="medium-feed-link"><a href="https://medium.com/awsfullstack/how-we-moved-our-whole-sagemaker-setup-to-another-aws-region-with-one-small-change-19aab59efffe?source=rss----a02b1eedcccf---4">Continue reading on AWS Fullstack »</a></p></div>]]></description>
            <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>
        </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>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/awsfullstack/your-train-test-split-is-probably-leaking-and-why-a-good-score-can-be-lying-8f3c16a97de6?source=rss----a02b1eedcccf---4"><img src="https://cdn-images-1.medium.com/max/2600/1*wKvDt9y5vRlnOe1--UNXCw.png" width="2800"></a></p><p class="medium-feed-snippet">Adding ten photos quietly moved most of our test photos into training. The model&#x2019;s score went up, and that&#x2019;s exactly why nobody noticed.</p><p class="medium-feed-link"><a href="https://medium.com/awsfullstack/your-train-test-split-is-probably-leaking-and-why-a-good-score-can-be-lying-8f3c16a97de6?source=rss----a02b1eedcccf---4">Continue reading on AWS Fullstack »</a></p></div>]]></description>
            <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>
        </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>
        <item>
            <title><![CDATA[Your ML Experiment Just Became Everyone’s Default (and How We Fixed It on SageMaker)]]></title>
            <link>https://medium.com/awsfullstack/your-ml-experiment-just-became-everyones-default-and-how-we-fixed-it-on-sagemaker-8863d50b704c?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/8863d50b704c</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[sagemaker]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Nishara Ramasinghe]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 15:13:42 GMT</pubDate>
            <atom:updated>2026-11-02T15:13:41.128Z</atom:updated>
            <content:encoded><![CDATA[<h4>Testing a new setting used to mean changing it for everyone. Here are the four simple rules that fixed it.</h4><p><em>Part 2 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*yWSx01PbaP5zP-0l3D7wHw.png" /></figure><p>Imagine a shared office with one thermostat. You feel cold, so you turn it up “just to test. &quot; It feels better, so you leave it. Now everyone in the office is living with your test, and three weeks later nobody remembers why it’s set to 26°C.</p><p>That’s what happened to our model training on <strong>AWS SageMaker</strong>. To try a new setting, we changed the default in the code. The test worked, so it stayed, and every training run after that quietly used our experiment. <strong>An experiment had become a rule</strong>, and nobody had decided that.</p><h3>How it used to work</h3><p>In our project, the training settings (how fast the model learns, how many rounds it trains for, and so on) lived in one place in the code. To try something new, you:</p><ul><li>Changed the number in the code.</li><li>Saved it and deployed it.</li><li>Started a training run.</li></ul><p>The run looked a bit better, so you moved on. But the next person to train a model, maybe days later, got <strong>your</strong> test value without knowing it was ever a test.</p><h3>The fix: settings for one run only</h3><p>Now each training run can carry its own changes. You pick the settings you want to try; they apply to <strong>that run only</strong>, and they’re saved with the run. The defaults don’t move unless someone changes them on purpose.</p><p>That part is simple. Four small rules around it are what made it safe.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OPPHphwDpcXaTR0sA3Vsuw.png" /></figure><h3>Rule 1: Reject setting names you don’t recognize.</h3><p>SageMaker’s built-in training <strong>silently ignores settings it doesn’t recognize.</strong> No error, no warning.</p><p>So if you mistype learning_rate as learning_rte, the job doesn&#39;t fail. It trains with the normal value, finishes, and reports a score under the name of an experiment that never actually happened. That&#39;s worse than a crash, because you&#39;d believe it.</p><p><strong>The fix:</strong> check every setting name against a list of allowed names before the job starts. If it’s not on the list, stop and say which names are allowed.</p><h3>Rule 2: Some settings are off-limits.</h3><p>A few settings must never be changed by hand, like the number of labels the model predicts or the image size. Change those, and you get a model that gives confident, wrong answers. So the system fills them in itself last, after everything else, so nothing can overwrite them.</p><p>Other settings are fine to change, except in one case. When you build on top of an earlier model to save time, the new run must match the old model’s shape. If it doesn’t, SageMaker only notices <strong>after the GPU has started billing</strong>, and the job crashes. So we block those changes up front, for that kind of run only.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ssGDgDXUCk6HUmWC9sDfiA.png" /></figure><h3>Rule 3: Save the full settings and what you changed.</h3><p>Each run saves two things: <strong>all</strong> the settings it used and <strong>only the ones you changed</strong>.</p><p>Saving both sounds like overkill. But defaults change over time. If you only saved the full list, then six weeks later you’d compare it to today’s defaults, and it would look like you changed things you never touched. Saving “what I changed” at the time avoids that confusion.</p><h3>Rule 4: Compare everything that changed side by side.</h3><p>This is the rule that matters most, and it applies to any team running experiments.</p><p>Say a run improved by <strong>1.4 points</strong> after you changed a setting. Great? Not necessarily. If <strong>400 new training images</strong> were also added since the last run, you can’t tell which one caused the improvement.</p><p>So our comparison screen shows three things together:</p><ul><li>How much the <strong>score</strong> changed.</li><li>Which <strong>settings</strong> changed.</li><li>How much <strong>new data</strong> was added.</li></ul><p>If both the settings and the data changed, it says so. A comparison that hides the data change would quietly give your settings the credit.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y3oVXWUnB7TiXIQ9DbwVuw.png" /></figure><h3>A small bonus: a word for “off”</h3><p>Our team wanted an option called “none” for image augmentation (randomly cropping or tweaking photos during training). SageMaker has no “none” value. The only way to turn it off is to leave the setting out completely, and if you send the word &quot;none,&quot; the job fails.</p><p>So when someone picks &quot;none,&quot; our system simply removes the setting before the job starts. It’s two lines of code, and it’s a good reminder: sometimes what a user means and what the API accepts are different things, and your code has to translate.</p><h3>Your checklist for safer experiments</h3><ol><li><strong>Does trying something change the default for everyone?</strong> If so, make it a per-run setting instead.</li><li><strong>Are setting names checked before the run starts?</strong></li><li><strong>Are the risky settings filled in by the system, not by hand?</strong></li><li><strong>Do you save what was changed at the time it was changed?</strong></li><li><strong>Does your comparison show the score, the settings, and the new data together?</strong></li></ol><h3>Final thoughts</h3><p>Trying something should never quietly change how everything works. Keep experiments separate from defaults, record what you changed, and always look at what else changed before you give a setting the credit.</p><p>Next in this series: why we left AWS’s labeling service, Ground Truth, before AWS shut it down, and what we built instead.</p><h3>References</h3><ul><li><a href="https://docs.aws.amazon.com/sagemaker/latest/dg/IC-Hyperparameter.html">Amazon SageMaker image classification settings (hyperparameters)</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, 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><strong>Your ML Experiment Just Became Everyone’s Default</strong> (you are here)</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><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=8863d50b704c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/your-ml-experiment-just-became-everyones-default-and-how-we-fixed-it-on-sagemaker-8863d50b704c">Your ML Experiment Just Became Everyone’s Default (and How We Fixed It 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[Aurora vs DynamoDB: Why We Switched, and What It Cost Us]]></title>
            <link>https://medium.com/awsfullstack/aurora-vs-dynamodb-why-we-switched-and-what-it-cost-us-8877fb8079ca?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/8877fb8079ca</guid>
            <category><![CDATA[dynamodb]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[aws-amplify]]></category>
            <category><![CDATA[amazon-aurora]]></category>
            <dc:creator><![CDATA[Vidura Senevirathne]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 05:01:02 GMT</pubDate>
            <atom:updated>2026-11-02T05:01:01.972Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Moving a multi-tenant Amplify Gen 2 app from Aurora Postgres to DynamoDB" src="https://cdn-images-1.medium.com/max/1024/1*YFJ316OD9i1fOEaSX3JhNA.png" /></figure><p>Our database wasn’t slow. It was in the way. We run a multi-tenant SaaS web app on Amplify Gen 2, with AppSync for the API and Lambda for the business logic. It started on Aurora Postgres, which felt like the safe choice. Queries were fine. The trouble sat around the database instead, in shared environments, network setup, and business rules that ran at the wrong moment.</p><p>In August 2026, we moved the whole data layer to DynamoDB. In this article, we will walk through why we switched, how we rebuilt it, what improved, what broke, and when Aurora is still the better choice.</p><h3>What Changes Between Aurora and DynamoDB?</h3><p>Here are the basics before the story.</p><figure><img alt="Aurora vs DynamoDB at a glance, comparing data model, queries, rules, networking, scaling, cost model and best fit" src="https://cdn-images-1.medium.com/max/1024/1*uhH4D8ZMNtEXP1QgXAKg8w.png" /></figure><p>Most of this article comes back to two rows of that table, the one about queries and the one about rules.</p><blockquote><em>Aurora lets you ask any question later. DynamoDB asks you to decide your questions now.</em></blockquote><h3>Where We Started: The Aurora Setup</h3><p>AppSync talked to Aurora through a schema file generated by npx ampx generate schema-from-database. Ours hardcoded the VPC, subnet and security-group IDs. So every developer pointed at the same cluster, inside the same VPC, using the same set of database roles.</p><p>The Lambdas ran inside that VPC too. Each one needed network permissions, SQL secrets, and SSM endpoints to reach its configuration from inside the private network.</p><p>The schema itself lived in about 20 SQL migration files. Business rules lived there too, written as database triggers.</p><h3>Why We Switched</h3><p>None of this showed up on a latency graph. It showed up in how hard the system was to work on.</p><ul><li><strong>No isolated environments:</strong> With one shared cluster, a schema change from one developer affected every other developer the moment it ran.</li><li><strong>Cold starts that looked like firewalls:</strong> The cluster paused when idle, and the first connection afterward timed out. The error looked like a firewall problem, not a sleeping database. AWS documents a <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html">typical resume time of about 15 seconds</a>, and 30 seconds or more after a pause longer than a day.</li><li><strong>A fragile setup:</strong> Connection strings broke whenever the password contained special characters, and Windows developers had to install Postgres tools locally.</li><li><strong>Triggers that ran too late:</strong> Our rules were AFTER triggers, which fire once the row is already written. That made them good at reacting and bad at refusing. The balance check could race under READ COMMITTED, where two concurrent writes both read the same balance and both pass. Deleting a record could also leave its linked claim record behind.</li><li><strong>A trust gap: </strong>The server accepted user IDs sent by the client instead of taking them from the login token.</li></ul><h4>Why Not Just Fix Aurora?</h4><p>It’s a fair question, and both obvious fixes exist. A cluster per developer would solve isolation, but every developer would then carry their own cluster, secrets, and resume delay, and the generated schema still pins one network configuration. The <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html">RDS Data API</a> puts an HTTPS endpoint in front of the cluster, so Lambdas no longer need to sit in the VPC. Neither fix changes when a rule runs or where the user ID comes from. Those two problems needed new code, whichever database sat underneath.</p><h3>How We Rebuilt It on DynamoDB</h3><p>The goal was to change the storage without changing what the frontend saw.</p><figure><img alt="Before and after architecture. Before, AppSync calls a VPC Lambda that connects to one shared Aurora cluster. After, AppSync calls DynamoDB tables directly, and custom mutations check rules before writing" src="https://cdn-images-1.medium.com/max/1024/1*X3Sy8dG7teBph5-e7dTZ7A.png" /></figure><h4>Same API, New Storage</h4><p>We kept all 31 models with the same names and fields, so the 96 existing frontend calls still compiled. That one decision lowered the risk of the switch more than anything else we did.</p><p>Uniqueness moved into the key. Of 22 UNIQUE constraints, 15 became part of a composite primary key. That also fixed two live bugs where a value was unique across all tenants instead of within one organization.</p><pre>// amplify/data/resource.ts (simplified, authorization omitted)<br>const schema = a.schema({<br>  Member: a<br>    .model({<br>      organizationId: a.id().required(),<br>      email: a.email().required(),<br>      role: a.string(),<br>    })<br>    .identifier([&#39;organizationId&#39;, &#39;email&#39;]),<br>});</pre><p>.identifier() makes the pair the primary key, and Amplify&#39;s generated create mutation refuses a key that already exists. A raw PutItem would quietly replace the item instead, which matters again below.</p><p>We added 20 GSIs for the reads we knew about. One is a sparse index that only holds open sessions, because DynamoDB leaves out any item missing the index key. A background job that used to scan the full table now queries it.</p><h4>Rules Before the Write</h4><p>Every trigger became a server-side custom mutation. Each one checks its rules before writing and reads the user from the verified token, never from the arguments (our <a href="https://awsfullstack.com/posts/authentication-vs-authorization-in-aws-amplify">authentication vs authorization guide</a> covers why).</p><p>The balance check is now one <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html">TransactWriteItems</a> call with a condition. DynamoDB checks the condition and applies the change atomically, so two concurrent requests can’t both spend the same balance. If the condition fails, nothing in the transaction is written.</p><pre>const userId = event.identity.sub; // from the token, never from args<br>await ddb.send(new TransactWriteCommand({<br>  TransactItems: [<br>    {<br>      Update: {<br>        TableName: ACCOUNTS_TABLE,<br>        Key: { accountId },<br>        UpdateExpression: &#39;SET balance = balance - :amount&#39;,<br>        ConditionExpression: &#39;balance &gt;= :amount&#39;,<br>        ExpressionAttributeValues: { &#39;:amount&#39;: amount },<br>      },<br>    },<br>    {<br>      Put: {<br>        TableName: LEDGER_TABLE,<br>        Item: { accountId, entryId, amount, createdBy: userId },<br>        ConditionExpression: &#39;attribute_not_exists(entryId)&#39;,<br>      },<br>    },<br>  ],<br>}));</pre><h4>Special Cases, Designed Around Their Reads</h4><p>The document-signing module uses a single-table design. Everything that belongs to one document lives in one partition, so loading a document is one Query instead of a six-way join.</p><p>The audit log is append-only, enforced by IAM. Its writer role can callPutItem, never UpdateItem or DeleteItem. IAM can&#39;t stop a PutItem from replacing an item with the same key, though, so every write also carries attribute_not_exists. That condition anchors a hash chain, with capped retries when two writers race for the same position.</p><p>Rate limiting moved from WAF to a TTL-based counter table. TTL deletion runs in the background and can lag, so a limiter like this has to compare timestamps rather than trust that expired items are gone.</p><h3>What Got Better</h3><p>The wins showed up in daily work.</p><ul><li><strong>Isolated sandboxes:</strong> Every developer gets their own, with just two sign-in keys to set up.</li><li><strong>Less to run:</strong> No VPC, cluster, connection strings, or SQL secrets to manage.</li><li><strong>Lower idle cost:</strong> We pay per request; nothing runs when idle, and dropping WAF and CloudTrail saves about $11 to $20 a month.</li><li><strong>Rules at write time:</strong> Every rule is checked before the write, and the user comes from the token.</li><li><strong>Predictable reads:</strong> Planned reads are fast and stay predictable.</li></ul><h3>Real Problems After the Move</h3><p>This is the section we wish someone had written for us. Several of these bugs returned something that looked like success.</p><h4>1. List pages scanned the whole table</h4><p>Some lists called list with a filter. In Amplify Data that runs aScan, which reads items first and filters afterward, so its cost grows with the table rather than with the result. We moved them to index queries with paging.</p><pre>// Before: Scan, filtered after the read<br>client.models.Session.list({ filter: { organizationId: { eq: orgId } } });<br>// After: Query on a GSI, one page at a time<br>client.models.Session.listSessionByOrganizationId(<br>  { organizationId: orgId },<br>  { limit: 50, nextToken },<br>);</pre><h4>2. A job returned nothing, though matches existed</h4><p>DynamoDB applies Limit to the items it reads, then filters them. So a page can hold zero matches while LastEvaluatedKey says more exist. Keep paging until that key is gone, or better, design an index that makes the filter unnecessary.</p><h4>3. Revoke ran, and nothing was revoked</h4><p>We stored revoked_at: null on new items. In DynamoDB, an attribute set to null still exists, with the NULL type. So every attribute_not_exists(revoked_at) check evaluated to false, and revoking silently did nothing.</p><h4>4. One field name broke the hash chain</h4><p>One code path wrote headSeq and another read head_seq. DynamoDB has no schema to object, so the hash-chain checkpoints broke without an error. Counting this one, we found 13 bugs after the move that our 263 unit tests had missed.</p><h4>5. The sync finished, and users saw “failed”</h4><p>AppSync caps <a href="https://docs.aws.amazon.com/general/latest/gr/appsync.html">request execution time at 30 seconds</a>. A long sync kept running and finished its work, but the request had already timed out. Work that can pass 30 seconds has to be started by the request and finished somewhere else.</p><h4>6. Deploys outgrew CloudFormation</h4><p>A nested stack hierarchy can create, update, or delete at most <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html">2,500 resources in one CloudFormation operation</a>. Our backend reached about 2,995, so a fresh deploy now takes two steps. (<a href="https://awsfullstack.com/posts/why-iam-policy-size-breaks-an-aws-deployment">IAM inline policy size</a> is another ceiling large Amplify backends hit.)</p><p>Permissions added a quieter delay. Changes can take minutes to take effect after a deploy, so a test run straight afterward can fail on access that’s already granted.</p><h3>Limitations and Trade-offs</h3><p>DynamoDB asks for decisions up front, and some of its costs never go away.</p><ul><li><strong>Access patterns come first:</strong> A new query usually means a new GSI.</li><li><strong>No joins or ad hoc SQL: </strong>Reporting and analytics get harder.</li><li><strong>GSIs are eventually consistent:</strong> A read right after a write can miss it.</li><li><strong>Denormalized counters drift:</strong> You need checks that catch it.</li><li><strong>Size limits:</strong> 400 KB per item, and a transaction holds up to 100 items or 4 MB.</li><li><strong>Security trade-offs we accepted:</strong> The rate limiter won’t stop a large DDoS the way WAF would, and the key-usage audit now runs weekly instead of in real time.</li></ul><h3>How to Choose, and What We’d Tell You</h3><p>Choose Aurora when reporting is heavy, when new queries show up often, or when your data is full of relationships you need to join.</p><p>Choose DynamoDB when you know your access patterns, your stack is already serverless, your traffic rises and falls, or your developers need isolated environments.</p><p>The lessons we’d pass on are short. List your queries before you design a table. Never scan where a user is waiting. Treat null and missing as different values, because DynamoDB does. And run end-to-end checks, because unit tests can’t catch everything.</p><p>DynamoDB didn’t make things simpler. It moved the complexity from running servers to designing data, and for us that was the better place for it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8877fb8079ca" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/aurora-vs-dynamodb-why-we-switched-and-what-it-cost-us-8877fb8079ca">Aurora vs DynamoDB: Why We Switched, and What It Cost Us</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[Build Your First AI Agent with Strands Agents in Under 30 Lines]]></title>
            <link>https://medium.com/awsfullstack/build-your-first-ai-agent-with-strands-agents-in-under-30-lines-23af88c8b6ed?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/23af88c8b6ed</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[generative-ai-solution]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[amazon-bedrock]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Binara Prabhanga]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 03:01:03 GMT</pubDate>
            <atom:updated>2026-11-02T03:01:03.111Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FJQL_aCOtBwWL6hQH2v09g.png" /></figure><p>Imagine you want an AI that does more than answer questions. You want it to check a value, run a calculation, or call one of your own functions, and then explain the result in plain language.</p><p>Your first thought might be to write the whole thing yourself. Call the model, read the reply, check whether it wants to use a tool, run that tool, send the result back, and repeat until it is done.</p><p>And yes, that works. But it is a lot of plumbing for a first project, and most of it has nothing to do with what you actually want to build.</p><p>In this article, I’ll show you an easier way. We will build a working AI agent on AWS using Strands Agents, an open-source Python SDK, in under 30 lines of code. You do not need any agent experience to follow along.</p><p>By the end, you will know:</p><ul><li>What an AI agent really is, in simple words</li><li>How to create your first agent in a few lines</li><li>How to give it your own tools</li><li>How the agent loop works behind the scenes</li><li>Where Strands fits next to Amazon Bedrock AgentCore</li></ul><h3>What is an AI agent?</h3><p>Before we write any code, let’s make sure we are talking about the same thing.</p><p>A normal chat model takes your question and gives you an answer. That is all it does. It cannot check anything, run anything, or call anything outside of itself.</p><p>An AI agent is different. It is a language model that can <strong>use tools</strong> and decide for itself which tool to use, when to use it, and when it is done.</p><p>Here is a simple way to see the difference:</p><ul><li><strong>Chat model:</strong> “How much does a $0.05 per hour server cost per month if it runs 8 hours a day?” It tries to work it out in its head and writes an answer.</li><li><strong>Agent:</strong> It sees that you have given it a cost calculator, calls that calculator with the right numbers, reads the result, and then explains it to you.</li></ul><p>The second one is more reliable because the maths is done by real code and not by a guess.</p><p>Every agent has three parts:</p><ol><li><strong>A model</strong>, which is the brain. It reads the request and makes decisions.</li><li><strong>Tools</strong>, which are the hands. These are normal functions, such as a calculator, an API call, or a database lookup.</li><li><strong>A prompt</strong>, which is the job description. It tells the agent what it is for and how to behave.</li></ol><p>That is it. Everything else in the agent world is a variation on these three.</p><h3>Can’t we just write the loop ourselves?</h3><p>Absolutely, you can. An agent is not magic. It is a loop around a model call.</p><p>But as always, there are a few things to consider.</p><p>To do it by hand, you need to:</p><ul><li>Send the conversation to the model</li><li>Check why the model stopped talking</li><li>If it asked for a tool, check that the arguments are valid</li><li>Find the right function and run it</li><li>Catch any errors and pass them back</li><li>Add the result to the conversation and call the model again</li><li>Know when to stop</li></ul><p>None of this is hard. But it is easy to get wrong, and you will rewrite it for every new project.</p><h3>Enter Strands Agents</h3><p>This is where Strands Agents comes into the picture.</p><p>Strands Agents is an open-source SDK from AWS for building AI agents. It runs that loop for you. You only describe three things: which model to use, which tools the agent can call, and what the agent is for.</p><p>A few things make it a good first choice:</p><ul><li><strong>Small API.</strong> A working agent is a few lines of Python.</li><li><strong>Plain Python tools.</strong> A tool is just a normal function with a decorator on top.</li><li><strong>Amazon Bedrock by default.</strong> If your AWS credentials can call a Bedrock model, you are close to ready. You can also plug in other model providers.</li><li><strong>Open source.</strong> You can read the code and see what it does.</li></ul><p>It also fits well with AWS’s bigger picture. In my <a href="https://medium.com/awsfullstack/amazon-bedrock-agentcore-explained-for-developers-cc7c78821583">Amazon Bedrock AgentCore article</a>, I explained that AgentCore works with different agent frameworks. Strands is one of the frameworks you can build with before you decide where to run it.</p><h3>Before you start</h3><p>You only need a few things in place:</p><ol><li><strong>Python 3.10 or newer.</strong> The Strands quickstart asks for this.</li><li><strong>An AWS account.</strong> A personal account is fine for learning.</li><li><strong>AWS credentials on your machine.</strong> The simplest way is to run aws configure with the AWS CLI. Environment variables, IAM roles, and Bedrock API keys also work.</li><li><strong>Access to a model in Amazon Bedrock.</strong> The Strands docs say you must enable model access for each model you plan to use. Open the Bedrock console in your region and check that the model you want is available to your account.</li><li><strong>A region where that model is available.</strong> The Strands docs set the region in code in their examples, so we will do the same.</li></ol><p>Two small tips before we continue.</p><p>First, never paste access keys into your code or commit them to Git. Let the AWS CLI or an IAM role handle credentials.</p><p>Second, every model call in Bedrock costs a little money. A few test prompts will cost very little, but it is a good habit to set up an AWS Budget alert before you experiment.</p><h3>Step 01: Set up your project</h3><p>Let’s start with a clean folder and a virtual environment, so the packages stay separate from the rest of your machine.</p><pre>python -m venv .venv<br>source .venv/bin/activate<br>pip install strands-agents</pre><p>On Windows, activate the environment with .venv\Scripts\activate instead.</p><p>That one package is enough for now.</p><h3>Step 02: Create your first agent</h3><p>Create a file called agent.py and add this:</p><pre>from strands import Agent</pre><pre>agent = Agent()<br>agent(&quot;Explain in two sentences what Amazon S3 is.&quot;)</pre><p>That is a complete agent. Run it with python agent.py.</p><p>The answer should appear in your terminal as the model writes it. Calling the agent also returns a result object, which is useful later when you want to use the answer in your own code.</p><p>So what just happened? With no settings at all, Strands picked a default model on Amazon Bedrock and sent your question to it. The Strands docs say the default model provider is Bedrock, and they name a Claude Sonnet model as the default.</p><p>If you see an access or region error, do not worry. It usually means your account cannot use that default model in your current region. We will fix that in Step 04 by choosing the model and region ourselves.</p><p>Right now, this is still just a chat model. It has no tools, so it cannot do anything except talk. Let’s change that.</p><h3>Step 03: Give your agent a tool</h3><p>Now for the interesting part.</p><p>In Strands, a tool is just a normal Python function with @tool on top. Let&#39;s build a small cost calculator, like the one we talked about earlier:</p><pre>from strands import Agent, tool</pre><pre>@tool<br>def monthly_cost(hourly_rate_usd: float, hours_per_day: float = 24) -&gt; float:<br>    &quot;&quot;&quot;Estimate the monthly cost of a resource, using a 30-day month.</pre><pre>    Args:<br>        hourly_rate_usd: The price per hour, in US dollars.<br>        hours_per_day: How many hours per day the resource runs.<br>    &quot;&quot;&quot;<br>    return round(hourly_rate_usd * hours_per_day * 30, 2)</pre><pre>agent = Agent(tools=[monthly_cost])<br>agent(&quot;A server costs $0.05 per hour. What does it cost per month if it runs 8 hours a day?&quot;)</pre><p>Run it again. This time the agent should call your function. The correct answer is $12.00, because 0.05 × 8 × 30 = 12.</p><p>Notice what you did not do. You never called monthly_cost yourself. The agent read the question, decided the tool was the right one, filled in the arguments, and used the result in its answer.</p><p>Three small details make this work:</p><ul><li><strong>The decorator.</strong> @tool tells Strands that this function is something the agent may call.</li><li><strong>The type hints.</strong> float and -&gt; float tell the model what kind of values go in and come out.</li><li><strong>The docstring.</strong> This is the most important part. The model reads it to understand what the tool does and when to use it.</li></ul><p>That last point is easy to miss. The model cannot see your code. It only sees the name, the types, and the description. If the description is vague, the agent will pick the wrong tool or skip it. The Strands docs give the same advice: read your descriptions from the model’s point of view, and make sure two tools never sound alike.</p><p>So write the docstring as if you were explaining the function to a new teammate.</p><h3>Step 04: Choose your own model and region</h3><p>So far, Strands picked the model for us. In real projects, you want to choose it yourself, for cost, speed, and quality. This is also the fix if you saw an access or region error earlier.</p><pre>from strands import Agent<br>from strands.models import BedrockModel</pre><pre>model = BedrockModel(<br>    model_id=&quot;global.anthropic.claude-sonnet-4-6&quot;,<br>    region_name=&quot;us-west-2&quot;,<br>    temperature=0.3,<br>)</pre><pre>agent = Agent(model=model)</pre><p>This is the same style the Strands quickstart uses. Here is what each setting means:</p><ul><li><strong>model_id:</strong> which model to call. Use the ID of a model that your account can use. Model names and IDs change over time, so copy the current one from the Bedrock console instead of trusting this article.</li><li><strong>region_name:</strong> the AWS region where you call Bedrock.</li><li><strong>temperature:</strong> how creative the answers are. A lower number gives more consistent answers, which is usually what you want when an agent is calling tools.</li></ul><p>Then pass the model to the agent with Agent(model=model), and add tools=[...] as before.</p><h3>Step 05: Try the built-in tools (optional)</h3><p>You do not have to write every tool yourself. Strands has a separate package with ready-made tools:</p><pre>pip install strands-agents-tools</pre><p>It includes tools for reading files, making HTTP requests, calling AWS services, running code in a sandbox, and more. You use them the same way as your own tools:</p><pre>from strands import Agent<br>from strands_tools import http_request</pre><pre>agent = Agent(tools=[http_request])</pre><p>A few honest notes before you use them:</p><ul><li>Some tools in that package are marked as deprecated in the project’s own tool list at the time of writing. Check the list before you depend on one. For something small, like our cost calculator, writing your own is often simpler.</li><li>Tools that run code or write files, such as shell, python_repl and file_writeask for your permission first. Leave that safety check on while you are learning.</li><li>The project describes these tools as experimental and recommends a security review before production use.</li></ul><h3>How does the agent loop work?</h3><p>In Step 03, the agent decided on its own to call your tool. Here is what happened behind the scenes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MN13Eubn5gEiDVoe0FxR6Q.png" /></figure><p>Each time around the loop, Strands sends the whole conversation to the model. If the model asks for a tool, Strands runs it and adds the result to the conversation. Then the model is called again. When the model stops asking for tools, its last message is your answer.</p><p>A few things are worth knowing:</p><ul><li><strong>The loop can run more than once.</strong> If a question needs two tools, or the same tool twice, the agent simply goes around again. Each pass adds to the conversation, so the model can build on earlier results.</li><li><strong>A failing tool does not crash the agent.</strong> If your function raises an error, the error goes back to the model as a result. The model can then try again, pick another tool, or explain the problem.</li><li><strong>The loop can end in other ways.</strong> For example, you can cancel it, the answer can be cut off by the token limit, or a safety filter can block the output. Your application should be ready for these cases.</li></ul><h3>The full agent in under 30 lines</h3><p>Here is everything together in one file. This is the agent we promised at the start, with a model, a tool, and a short job description (the system prompt):</p><pre>from strands import Agent, tool<br>from strands.models import BedrockModel<br></pre><pre>@tool<br>def monthly_cost(hourly_rate_usd: float, hours_per_day: float = 24) -&gt; float:<br>    &quot;&quot;&quot;Estimate the monthly cost of a resource, using a 30-day month.</pre><pre>    Args:<br>        hourly_rate_usd: The price per hour, in US dollars.<br>        hours_per_day: How many hours per day the resource runs.<br>    &quot;&quot;&quot;<br>    return round(hourly_rate_usd * hours_per_day * 30, 2)<br></pre><pre>model = BedrockModel(<br>    model_id=&quot;global.anthropic.claude-sonnet-4-6&quot;,<br>    region_name=&quot;us-west-2&quot;,<br>    temperature=0.3,<br>)</pre><pre>agent = Agent(<br>    model=model,<br>    tools=[monthly_cost],<br>    system_prompt=&quot;You are a friendly cloud cost helper. Use the tool for any cost maths.&quot;,<br>)</pre><pre>agent(&quot;A server costs $0.05 per hour. What does it cost per month if it runs 8 hours a day?&quot;)</pre><p>That is 28 lines, blank lines included.</p><p>Run it with python agent.py. The exact words will be different each time, but the answer should come out as <strong>$12.00 per month</strong>.</p><p>Now try a follow-up on the same agent to see it reuse the tool:</p><pre>agent(&quot;And what if it runs all day, every day?&quot;)</pre><p>This time the answer should be <strong>$36.00</strong>, because 0.05 × 24 × 30 = 36. The agent remembers the earlier question, so you do not need to repeat the price.</p><p>That is a real agent. It has a brain, a tool, and a job, and it decides for itself when to use the tool.</p><h3>Common beginner mistakes</h3><p>Most first-time problems come from the same few places. Here are the ones to check first.</p><ol><li><strong>The model is not enabled for your account.</strong> You get an access error even though your code is fine. Open the Bedrock console, check model access in your region, and make sure your IAM user or role is allowed to invoke the model.</li><li><strong>The region does not have that model.</strong> Not every model is available in every region. If one region fails, check the model’s availability, or change region_name.</li><li><strong>No AWS credentials found.</strong> Python cannot see your login. Run aws configure, or use environment variables or an IAM role. Do not hard-code keys.</li><li><strong>A vague tool description.</strong> If the agent ignores your tool or picks the wrong one, rewrite the docstring first. Say what the tool does, what each argument means, and when to use it.</li><li><strong>Two tools that sound the same.</strong> The model chooses between tools by reading their descriptions. If two descriptions overlap, it is guessing. Make each one clearly different.</li><li><strong>Turning off safety checks to save time.</strong> Strands tools that run code or write files ask for your permission. There is a setting to skip that check, but do not use it while you are learning.</li></ol><h3>Where does Strands fit with AgentCore?</h3><p>If you read my earlier article, you may be wondering how this connects to Amazon Bedrock AgentCore.</p><p>The short answer is that they solve different problems:</p><ul><li><strong>Strands Agents</strong> is how you <strong>build</strong> the agent. It is the code: the model, the tools, and the loop.</li><li><strong>Amazon Bedrock AgentCore</strong> is where you <strong>run and manage</strong> the agent in production. It gives you a place to run it, memory, secure tool access, identity, monitoring, and more.</li></ul><p>You can learn Strands on your laptop first, with no extra services. When the agent starts to need real users, permissions, and monitoring, that is when AgentCore becomes useful. I explained that side in <a href="https://medium.com/awsfullstack/amazon-bedrock-agentcore-explained-for-developers-cc7c78821583">Amazon Bedrock AgentCore Explained for Developers</a>.</p><p>AWS is also adding more around this area. For example, the <a href="https://aws-news.com/article/2026-10-13-aws-weekly-roundup-prime-day-2026-by-the-numbers-claude-haiku-55-amazon-nova-25-sonic-and-more-october-13-2026">October 13, 2026 AWS weekly roundup</a> mentions Strands Box, open-source sandboxes for running agents more safely.</p><h3>Final thoughts</h3><p>Building an AI agent sounds like a big topic. But the core idea is small.</p><p>You give a model some tools, you tell it what its job is, and a loop does the rest. Strands Agents takes care of that loop, so you can spend your time on the tools and the problem you actually want to solve.</p><p>In this article, we:</p><ul><li>Learned what an AI agent is</li><li>Created a first agent in three lines</li><li>Added our own tool with @tool</li><li>Chose a model and region with BedrockModel</li><li>Put it all together in 28 lines</li></ul><p>A good next step is to replace monthly_cost with a tool that does something useful in your own project, such as checking a status, looking up a record, or calling an internal API. Start with one tool, test it, and then add more.</p><p>Cheers!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=23af88c8b6ed" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/build-your-first-ai-agent-with-strands-agents-in-under-30-lines-23af88c8b6ed">Build Your First AI Agent with Strands Agents in Under 30 Lines</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[Why Open Web RAG Fails in AWS Bedrock]]></title>
            <link>https://medium.com/awsfullstack/why-open-web-rag-fails-in-aws-bedrock-144941a3a701?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/144941a3a701</guid>
            <category><![CDATA[web-rag]]></category>
            <category><![CDATA[user-location-controls]]></category>
            <category><![CDATA[aws-bedrock]]></category>
            <category><![CDATA[domain-filtering]]></category>
            <dc:creator><![CDATA[Maleesha Nuwanthi Kolombage]]></dc:creator>
            <pubDate>Thu, 29 Oct 2026 13:31:01 GMT</pubDate>
            <atom:updated>2026-10-29T13:31:01.769Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wed0lYQcIsLIfFZlhPPaDg.png" /><figcaption>Why Open Web RAG Fails in AWS Bedrock</figcaption></figure><p>Building production-ready GenAI applications requires grounding your Large Language Models (LLMs) in real-time information. While static knowledge bases handle internal documentation well, applications often need to query the live web to fetch up-to-date data like regional product availability, breaking technical updates, or local compliance laws.</p><p>To solve this, developers rely on Web Retrieval-Augmented Generation (Web RAG).</p><p>However, untamed Web RAG introduces massive architectural vulnerabilities. If you let an LLM loose on the open internet, it will eventually pull data from low-quality forums, outdated blogs, or incorrect geographical regions, leading directly to hallucinations and confidently incorrect answers.</p><p>AWS just introduced a native fix for this engineering bottleneck.</p><p>Directly configured inside the web search tool payload, AWS Bedrock now supports <strong>Domain Filtering</strong> and <strong>User Location Controls</strong>. Here is a breakdown of why open-web RAG breaks at scale, and how to implement these new native boundaries to keep your enterprise AI agents accurate.</p><h3>The Main Problem: The Chaos of Unfiltered Web RAG</h3><p>When an AI model uses an open-web search tool to ground its answer, it typically behaves like a standard search engine without a human operator. It fetches the top structural results for a query and injects that context directly into the model prompt context window.</p><p>This introduces three critical points of failure:</p><ul><li><strong>The Hallucination Vector:</strong> If a competitor’s site, a random Reddit thread, or a deprecated documentation page ranks highly for a specific query, the LLM will accept it as factual grounding text.</li><li><strong>Geographic Misalignment:</strong> Asking an LLM agent about “local logistics regulations” or “regional service availability” is useless if the underlying search engine defaults to a generic server region instead of the user’s actual location.</li><li><strong>Middleware Overhead:</strong> Up until now, preventing these issues required developers to write complex custom middleware to get search API results, parse URLs, filter out bad domains, and manually spoof headers for geolocation before passing clean text back to Bedrock.</li></ul><h3>The Solution: Native Bedrock Search Constraints</h3><p>AWS has eliminated the need for custom preprocessing middleware by embedding structural search constraints directly into the Bedrock execution loop via the OpenAI Responses API.</p><h4>1. Domain Filtering</h4><p>You can now explicitly guide or restrict the model’s data sourcing by defining hard structural limits. The API allows you to pass up to 100 allowed domains to enforce strict whitelist restrictions (e.g., only pulling from your official documentation or trusted .gov entities). Conversely, you can specify up to 100 blocked domains to blacklist known problematic sites, outdated forums, or competitor pages.</p><h4>2. User Location Controls</h4><p>Instead of getting generic search results tied to the physical AWS data center hosting your infrastructure, you can now pass granular location parameters directly with the request metadata. This forces the underlying web search tool to retrieve geographically relevant snippets essential for bots handling localized pricing, local logistics, or regional product launches.</p><h3>Architectural Implementation: The API Payload</h3><p>These controls are configured directly within the existing web_search tool object. Below is a simulated example of how to implement both domain filtering and location parameters inside your standard Bedrock API request payload:</p><pre>{<br>  &quot;model&quot;: &quot;amazon.titan-text-premier-v1&quot;,<br>  &quot;messages&quot;: [<br>    {<br>      &quot;role&quot;: &quot;user&quot;,<br>      &quot;content&quot;: &quot;What are the latest compliance guidelines and registration requirements for commercial drone operators?&quot;<br>    }<br>  ],<br>  &quot;tools&quot;: [<br>    {<br>      &quot;type&quot;: &quot;web_search&quot;,<br>      &quot;web_search&quot;: {<br>        &quot;filters&quot;: {<br>          &quot;allowed_domains&quot;: [<br>            &quot;caa.lk&quot;,<br>            &quot;gov.lk&quot;,<br>            &quot;://amazon.com&quot;<br>          ],<br>          &quot;blocked_domains&quot;: [<br>            &quot;unverifiedblogpost.com&quot;,<br>            &quot;outdatedforum.net&quot;<br>          ]<br>        },<br>        &quot;user_location&quot;: {<br>          &quot;country_code&quot;: &quot;LK&quot;,<br>          &quot;region&quot;: &quot;Western Province&quot;,<br>          &quot;city&quot;: &quot;Colombo&quot;<br>        }<br>      }<br>    }<br>  ]<br>}</pre><h3>Why this payload structure works:</h3><ul><li><strong>Zero Custom Scrapers:</strong> The whitelists and blacklists are enforced at the retrieval layer before data ever hits your application memory or the model’s context window.</li><li><strong>Context Preservation:</strong> By targeting the search specifically to Colombo, Sri Lanka, and whitelisting .lk domains, the model completely bypasses generic global regulations (like FAA rules in the US or CAA rules in the UK). This drastically reduces token wastage on irrelevant international text and eliminates geographic hallucinations.</li></ul><h3>Regional Availability</h3><p>As of 23rd October 2026, native domain filtering and location controls are deployed across standard enterprise AWS environments, including:</p><ul><li>US East (N. Virginia)</li><li>US East (Ohio)</li><li>US West (Oregon)</li><li>AWS GovCloud (US-West)</li></ul><p>a massive win for highly secure public sector applications that require strict domain whitelists.</p><p>Building reliable GenAI isn’t just about picking the largest model; it’s about building deterministic guardrails around how that model consumes data. By locking down your web search boundaries directly at the tool layer, you ensure your application stays accurate, localized, and safe from internet noise.</p><p>For precise pricing metrics and architectural constraints, refer to the Web Search configuration segments inside the official <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/web-search.html?refid=79d4aa9f-6055-4b3c-801b-6693f3e78224">AWS Bedrock User Guide</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=144941a3a701" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/why-open-web-rag-fails-in-aws-bedrock-144941a3a701">Why Open Web RAG Fails in AWS Bedrock</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[DynamoDB Data Modeling: How to Design Tables for Scalability and Performance]]></title>
            <link>https://medium.com/awsfullstack/dynamodb-data-modeling-how-to-design-tables-for-scalability-and-performance-43f0b857c139?source=rss----a02b1eedcccf---4</link>
            <guid isPermaLink="false">https://medium.com/p/43f0b857c139</guid>
            <category><![CDATA[database-design]]></category>
            <category><![CDATA[data-modelling]]></category>
            <category><![CDATA[nosql]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[dynamodb]]></category>
            <dc:creator><![CDATA[Tharaka Chathuranga]]></dc:creator>
            <pubDate>Thu, 29 Oct 2026 12:46:01 GMT</pubDate>
            <atom:updated>2026-10-29T12:46:01.431Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>Learn how to model data in Amazon DynamoDB using access patterns, partition keys, sort keys, and GSIs with practical examples.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nlqKZV-2kYD3Si3JeRrQJg.png" /><figcaption><strong>How to Design Scalable DynamoDB Tables</strong></figcaption></figure><p>Imagine you are building an online store. Customers browse products, place orders, and check their purchase history. At the same time, admins need to look up orders and monitor the ones that are still being processed.</p><p>The real question is: how should you organize that data?</p><p>If you are used to relational databases, your first instinct may be to create separate tables and connect them with foreign keys. But DynamoDB works differently. Instead of designing the tables first, you start by thinking about the queries your application needs to support. Your key design determines which queries DynamoDB can handle efficiently.</p><h3>Start With Access Patterns</h3><p>Before designing a DynamoDB table, first understand how the application will use the data. In our store, customers need to view their profile and order history. Admins need to find individual orders and monitor orders that are still being processed.</p><p>Each of these is an <strong>access pattern</strong>. It describes how the application reads or writes data. Writing these access patterns down early makes the rest of the design much easier.</p><p>For our shopping application, the main access patterns are:</p><ul><li><strong>Customer profile:</strong> Find a customer by customer ID.</li><li><strong>Recent orders:</strong> Get the latest 20 orders for a customer.</li><li><strong>Order history:</strong> Find a customer’s orders within a date range.</li><li><strong>Order lookup:</strong> Find an order using its order ID.</li><li><strong>Open orders:</strong> List orders that are still being processed.</li></ul><h3>Why Do These Details Matter?</h3><p>Compare these two requirements:</p><ul><li><strong>Get customer orders.</strong></li><li><strong>Get the latest 20 orders for a customer, newest first.</strong></li></ul><p>The first tells us what data we want, but not much about how it should be returned. The second gives us more useful information.</p><p>We are retrieving orders for one customer, so the customer ID is a good candidate for the partition key. We also want the newest orders first, so the order timestamp belongs in the sort key. Now the key design begins to take shape.</p><p>With the customer ID grouping the orders and the timestamp keeping them in order, DynamoDB can return the latest orders directly without scanning the entire table.</p><p>Frequency matters too. A query that runs thousands of times an hour deserves more attention than an admin report that runs once a day. The goal is not only to know what data the application needs. You also need to understand how often that data will be requested and how it needs to be returned.</p><h3>Design Keys Around Your Queries</h3><p>Once the access patterns are clear, the key design usually becomes much easier.</p><h4>Start With Customer Profiles</h4><p>The first requirement is simple: find a customer by ID. A Customers table can use customerId as the partition key.</p><pre>Table: Customers<br><br>customerId<br>----------<br>123<br>456<br>789</pre><p>Every customer has a unique ID, so a GetItem request using customerId = 123 can return that customer’s profile directly. There is no need for a sort key here.</p><h3>Design Keys for Customer Orders</h3><p>Orders are a little more interesting. One customer can place many orders, and we need to retrieve those orders efficiently.</p><p>For this, we can create an Orders table with a partition key and a sort key.</p><pre>Table: Orders<br><br>PK             SK<br>-------------  ------------------------------<br>CUSTOMER#123   2026-09-10T08:12:44Z#A102<br>CUSTOMER#123   2026-09-18T19:03:10Z#A117<br>CUSTOMER#123   2026-10-02T14:31:07Z#A145</pre><p>The partition key groups all orders belonging to the same customer. The sort key contains the order timestamp followed by the order ID. The CUSTOMER# prefix is simply a naming convention that makes the purpose of the key easier to recognize when reading the data.</p><p>This structure supports several access patterns:</p><ul><li><strong>Get customer orders:</strong> Query with PK = CUSTOMER#123.</li><li><strong>Get recent orders:</strong> Use ScanIndexForward: false and Limit: 20.</li><li><strong>Get orders within a date range:</strong> Use a BETWEEN condition on the sort key with timestamp boundaries.</li></ul><p>The customer ID finds the group of orders, while the timestamp determines their order. Each part of the key has a clear job.</p><h3>An Alternative: Single-Table Design</h3><p>Now imagine the application often needs a customer’s profile and their orders together. Instead of keeping them in separate tables, you could store both types of items in the same table. This is one example of <strong>single-table design</strong>.</p><pre>Table: Ecommerce<br><br>PK             SK<br>-------------  ----------------------------------<br>CUSTOMER#123   PROFILE<br>CUSTOMER#123   ORDER#2026-09-10T08:12:44Z#A102<br>CUSTOMER#123   ORDER#2026-09-18T19:03:10Z#A117<br>CUSTOMER#123   ORDER#2026-10-02T14:31:07Z#A145</pre><p>All four items share the same partition key, which creates an item collection for the customer. The sort key prefixes, such as PROFILE and ORDER#, distinguish the different item types.</p><p>With this structure, you can:</p><ul><li>Get the profile and orders together by querying PK = CUSTOMER#123.</li><li>Get only the orders with begins_with(SK, “ORDER#”).</li><li>Get only the profile with GetItem using PK = CUSTOMER#123 and SK = PROFILE.</li></ul><p>Both separate-table and single-table designs can work. Separate tables are often easier to understand when different entities have very different access patterns. Single-table design can be useful when related data is frequently read together.</p><p>The important point is not to force everything into one table or always separate everything into multiple tables. Choose the structure that best supports the application’s access patterns.</p><h3>Make Sort Keys Work for You</h3><p>Let’s return to the Orders table. We used the customer ID as the partition key and combined the order timestamp with the order ID in the sort key.</p><pre>PK = CUSTOMER#123<br>SK = 2026-10-02T14:31:07Z#A145</pre><p>CUSTOMER#123 tells us which customer the order belongs to. The sort key tells us when the order was created and includes the unique order ID, A145.</p><p>Why include the order ID? Two orders could be created at the same recorded timestamp. If the timestamp were the entire sort key, both items would end up with the same composite primary key. Adding the order ID keeps each order unique.</p><p>DynamoDB returns items with the same partition key in sort-key order. Because these timestamps use a consistent ISO 8601 format, their string order also follows chronological order. That makes it easy to retrieve the newest orders first.</p><p>For example:</p><pre>const result = await db.send(<br>  new QueryCommand({<br>    TableName: &quot;Orders&quot;,<br>    KeyConditionExpression: &quot;PK = :pk&quot;,<br>    ExpressionAttributeValues: {<br>      &quot;:pk&quot;: &quot;CUSTOMER#123&quot;,<br>    },<br>    ScanIndexForward: false,<br>    Limit: 20,<br>  })<br>);</pre><p>The partition key limits the query to one customer’s orders. ScanIndexForward: false reverses the normal sort-key order, so the newest orders are returned first.</p><p>Limit: 20 tells DynamoDB to evaluate up to 20 items for that request. A single Query response is also limited to 1 MB of data. If DynamoDB has more items to return, the response includes a LastEvaluatedKey, which can be used to continue the query on the next page.</p><p>The same sort-key structure can also support date-range queries with BETWEEN, as long as the range boundaries follow the same timestamp#orderId format.</p><h3>Model Relationships Without Joins</h3><p>So far, we have worked with customers and orders. But orders also contain products. One order can contain multiple products, and the same product can appear in many different orders.</p><p>That gives us a many-to-many relationship.</p><p>In a relational database, you might handle this with a join table. DynamoDB does not provide joins through the Query API, so relationships are usually modeled directly in the data.</p><p>One option is an OrderItems table.</p><pre>Table: OrderItems<br><br>PK          SK<br>----------- -------------<br>ORDER#A145  PRODUCT#P9<br>ORDER#A145  PRODUCT#P31<br>ORDER#A153  PRODUCT#P9</pre><p>Each item represents one relationship between an order and a product. The partition key identifies the order, while the sort key identifies the product.</p><p>You can also store extra information on the item, such as quantity or the price paid at the time of purchase.</p><p>Querying:</p><pre>PK = ORDER#A145</pre><p>Returns every product in that order.</p><p>That handles one direction of the relationship. But what if we want to ask the reverse question: which orders contain product P9?</p><p>The existing primary key cannot answer that efficiently. That is where a Global Secondary Index becomes useful.</p><h3>Use GSIs for Additional Queries</h3><p>Sometimes the table’s primary key is not enough for every query your application needs.</p><p>A <strong>Global Secondary Index</strong>, or GSI, gives you another way to query the same data using a different key structure.</p><p>For the OrderItems table, the primary key lets us start with an order and find its products.</p><pre>Base table: OrderItems<br><br>PK          SK<br>----------- -------------<br>ORDER#A145  PRODUCT#P9<br>ORDER#A145  PRODUCT#P31<br>ORDER#A153  PRODUCT#P9</pre><p>But what if we need the reverse query: <strong>which orders contain product P9?</strong></p><p>We can add a GSI that uses the product as the partition key and the order as the sort key.</p><pre>GSI: ProductOrdersIndex<br><br>Index PK      Index SK<br>------------  -------------<br>PRODUCT#P9    ORDER#A145<br>PRODUCT#P9    ORDER#A153<br>PRODUCT#P31   ORDER#A145</pre><p>Now a query with:</p><pre>Index PK = PRODUCT#P9</pre><p>returns the orders that contain that product.</p><p>The important point is that the base table has not changed. The GSI simply gives the application another access path to the same items.</p><p>The same idea can support other queries too. For example, if the Orders table is grouped by customer ID, a GSI with orderId as its partition key lets you find an order A145 without knowing which customer placed it.</p><p>GSIs are useful, but they also add cost. They use additional storage, receive updates when indexed data changes, and reads from a GSI are eventually consistent. You also need to think about traffic distribution. If many requests target the same index key, such as OPEN, that key can become heavily concentrated.</p><blockquote><em>The rule is simple: </em><strong><em>use a GSI when an important access pattern cannot be handled efficiently by the table’s primary key.</em></strong></blockquote><h3>How It All Comes Together</h3><p>The pieces now fit together. Partition keys decide which group of items DynamoDB should look at. Sort keys organize items within that group and allow ordered or range-based queries. GSIs provide additional access paths when the primary key is not enough.</p><p>For our shopping application, we created structures for customers, orders, and order-product relationships. We also explored single-table design, where related records share the same table and partition so they can be retrieved together.</p><p>No single design works for every application. Separate tables can be easier to understand and maintain, while single-table design can be useful when related data is frequently accessed together.</p><p>What matters is whether the design supports the queries your application needs as traffic and data volume grow.</p><h3>The Key Takeaway</h3><p>Before designing a DynamoDB table, keep these points in mind:</p><ul><li><strong>Start with access patterns.</strong> Understand what the application needs to read and write before choosing the keys.</li><li><strong>Give each key a purpose.</strong> Use partition keys to group related data and sort keys to support ordering and range queries.</li><li><strong>Use GSIs when the primary key is not enough.</strong> They provide additional ways to access the same data.</li><li><strong>Think about traffic and growth.</strong> Consider how requests will be distributed across keys and how the design will behave as the application grows.</li></ul><p>In DynamoDB, keys are more than identifiers. They are the access paths your application depends on, and designing them well starts before you write the first item.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=43f0b857c139" width="1" height="1" alt=""><hr><p><a href="https://medium.com/awsfullstack/dynamodb-data-modeling-how-to-design-tables-for-scalability-and-performance-43f0b857c139">DynamoDB Data Modeling: How to Design Tables for Scalability and Performance</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>