<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oluwasegun Adedigba</title>
    <description>The latest articles on DEV Community by Oluwasegun Adedigba (@cloudsege).</description>
    <link>https://dev.to/cloudsege</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1037830%2F5768f8f1-81ca-4d33-bcd1-0d9312b2451c.jpg</url>
      <title>DEV Community: Oluwasegun Adedigba</title>
      <link>https://dev.to/cloudsege</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudsege"/>
    <language>en</language>
    <item>
      <title>Non-functional Application Requirements: Sustainability</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 11 Nov 2026 15:03:45 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-sustainability-3be8</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-sustainability-3be8</guid>
      <description>&lt;p&gt;On 7 July 2026, Ireland's statistics office published a number worth sitting with. In 2025, data centers used &lt;strong&gt;23%&lt;/strong&gt; of the country's metered electricity. That's more than every urban home in Ireland combined, at 18% (&lt;a href="https://www.cso.ie/en/releasesandpublications/ep/p-dcmec/datacentresmeteredelectricityconsumption2025/keyfindings/" rel="noopener noreferrer"&gt;CSO, 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That same summer, Amazon's 2025 sustainability report said its absolute emissions rose 16% in a year. The same report says Amazon matched 100% of its electricity with renewable energy, for the third year running.&lt;/p&gt;

&lt;p&gt;Both are true, because they measure different things. If you can't tell which is which, you can't tell whether your own architecture choices helped.&lt;/p&gt;

&lt;p&gt;So this finale is built around a panel. I didn't interview anyone. I took the questions I'd ask and answered them from what six experts have already published.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sustainability is &lt;strong&gt;energy, carbon and water per unit of useful work&lt;/strong&gt;. Track a resource proxy (vCPU-minutes per transaction) weekly and carbon monthly.&lt;/li&gt;
&lt;li&gt;Know which carbon number you're reading: location-based or market-based, and which scope.&lt;/li&gt;
&lt;li&gt;AWS owns sustainability &lt;em&gt;of&lt;/em&gt; the cloud. You own sustainability &lt;em&gt;in&lt;/em&gt; it: fewer resources, busy for less time, on efficient chips, in a well-chosen Region.&lt;/li&gt;
&lt;li&gt;AWS carbon data is monthly and stops at account, Region and service. Nothing gives you carbon per model call.&lt;/li&gt;
&lt;li&gt;For AI, inference is the part you control. The smallest adequate model, caching and batch cut cost and carbon together. Sometimes the right model is a regex.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What: sustainability you can test
&lt;/h2&gt;

&lt;p&gt;Sustainability is the requirement to keep the environmental cost of each unit of useful work low for the life of the workload. Three quantities get mixed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Energy&lt;/strong&gt; is the kilowatt-hours your resources draw, busy or idle. &lt;strong&gt;Carbon&lt;/strong&gt; is that energy times the grid's carbon intensity, plus the emissions from making the hardware. &lt;strong&gt;Water&lt;/strong&gt; goes into cooling and power generation. A cleaner grid cuts carbon without saving a kilowatt-hour, so report them separately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjltblbwtvs5h9rv0gad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjltblbwtvs5h9rv0gad.png" alt="Three numbers, three meanings. Energy: the kilowatt-hours your resources draw. Carbon: energy times the grid carbon intensity, plus emissions from making the hardware. Water: goes into cooling and power generation." width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GHG Protocol sorts emissions into &lt;strong&gt;scopes&lt;/strong&gt;: Scope 1 is fuel burned on site, Scope 2 is bought electricity, Scope 3 is everything else, including making the servers. What AWS emits running your workload lands in your Scope 3. Then there's method. &lt;strong&gt;Location-based&lt;/strong&gt; carbon uses the average grid where the power was used. &lt;strong&gt;Market-based&lt;/strong&gt; carbon credits the renewable contracts a company buys. "100% matched" is a market-based claim.&lt;/p&gt;

&lt;p&gt;To make it testable, the Green Software Foundation's &lt;strong&gt;Software Carbon Intensity (SCI)&lt;/strong&gt; spec, now ISO/IEC 21031:2024, gives you a rate:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SCI = ((E × I) + M) per R&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;E is energy, I is grid carbon intensity, M is the hardware's share of embodied emissions and R is your functional unit, like one API call. Offsets and renewable certificates can't lower it. Only cutting emissions can.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  E["E&amp;lt;br/&amp;gt;energy"] --&amp;gt; EI["E x I&amp;lt;br/&amp;gt;carbon from the energy used"]
  I["I&amp;lt;br/&amp;gt;grid carbon intensity"] --&amp;gt; EI
  EI --&amp;gt; T["Total emissions"]
  M["M&amp;lt;br/&amp;gt;hardware share of&amp;lt;br/&amp;gt;embodied emissions"] --&amp;gt; T
  T --&amp;gt;|"per R"| SCI["SCI&amp;lt;br/&amp;gt;carbon per functional unit&amp;lt;br/&amp;gt;like one API call"]
  X["Offsets and renewable&amp;lt;br/&amp;gt;certificates"] -.-&amp;gt;|"can't lower it"| SCI&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The score only falls when energy, grid intensity or embodied emissions per unit of work fall.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You won't know E for a managed service, so AWS Well-Architected suggests a &lt;strong&gt;proxy KPI&lt;/strong&gt;: resources provisioned per unit of work, like vCPU-minutes per transaction. It moves within days of a change. The carbon report catches up weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: the grid can't keep up
&lt;/h2&gt;

&lt;p&gt;Data center demand is outgrowing the grids that feed it. The IEA's April 2025 &lt;em&gt;Energy and AI&lt;/em&gt; report put data centers at about 415 TWh in 2024, roughly 1.5% of global electricity. Its 2026 follow-up, &lt;em&gt;Key Questions on Energy and AI&lt;/em&gt;, says demand grew 17% in 2025 to about 485 TWh, against 3% for electricity overall, with AI-focused data centers up 50%. It expects roughly 950 TWh by 2030.&lt;/p&gt;

&lt;p&gt;Hardware matters too. Amazon's report puts supply-chain emissions, including data center construction and hardware, at 76% of its footprint, up 20% in a year. Every idle server you keep carries a slice of that.&lt;/p&gt;

&lt;p&gt;The good news: almost every sustainability lever is also a cost lever, so savings fund the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel: six published answers
&lt;/h2&gt;

&lt;p&gt;Each answer paraphrases published work, with a link and at most one short quote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do I start?&lt;/strong&gt; &lt;em&gt;Adrian Cockcroft, former VP of Sustainability Architecture at Amazon.&lt;/em&gt; His QCon London 2023 talk boils developer advice down to "use fewer computers, and use them for less time" (&lt;a href="https://www.infoq.com/presentations/cloud-sustainability-green-energy" rel="noopener noreferrer"&gt;InfoQ, 2023&lt;/a&gt;). He prefers location-based data for tuning, and AWS has published it since June 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I measure honestly?&lt;/strong&gt; &lt;em&gt;Asim Hussain, executive director of the Green Software Foundation until March 2026.&lt;/em&gt; The SCI is built so a score falls only when emissions do. In a 2023 interview he called carbon-aware scheduling a cheap way to save a few percent, not the big win. He also says green software is "more efficient, which translates almost always to being faster" (&lt;a href="https://www.technologyreview.com/2023/12/18/1085390/developing-climate-solutions-with-green-software/" rel="noopener noreferrer"&gt;MIT Technology Review, 2023&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which practices matter?&lt;/strong&gt; &lt;em&gt;Anne Currie, Sarah Hsu and Sara Bergman, authors of&lt;/em&gt; Building Green Software &lt;em&gt;(O'Reilly, 2024).&lt;/em&gt; Their chapters give you three buckets for any change (&lt;a href="https://www.oreilly.com/library/view/building-green-software/9781098150617/" rel="noopener noreferrer"&gt;O'Reilly&lt;/a&gt;): code efficiency (less compute per task), hardware efficiency (fewer devices, kept longer) and carbon awareness (run work when and where the grid is clean).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training or inference?&lt;/strong&gt; &lt;em&gt;David Patterson and Jeff Dean, Google.&lt;/em&gt; Their 2022 paper reports inference at about three-fifths of Google's machine-learning energy (&lt;a href="https://arxiv.org/abs/2204.05149" rel="noopener noreferrer"&gt;arXiv, 2022&lt;/a&gt;). A Meta team led by Carole-Jean Wu saw a similar 10:20:70 split across experimentation, training and inference. Patterson and Dean also co-authored Google's 2025 Gemini study: 0.24 Wh per median text prompt, after a 33x drop in a year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does model choice matter?&lt;/strong&gt; &lt;em&gt;Sasha Luccioni, then at Hugging Face, now co-founder of Sustainable AI Group.&lt;/em&gt; With Yacine Jernite and Emma Strubell, she measured 1,000 inferences across many tasks. General-purpose generative models were "orders of magnitude more expensive than task-specific systems", even at similar sizes (&lt;a href="https://arxiv.org/abs/2311.16863" rel="noopener noreferrer"&gt;ACM FAccT, 2024&lt;/a&gt;). Her AI Energy Score now rates models one to five stars per task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it getting better?&lt;/strong&gt; &lt;em&gt;Fatih Birol, Executive Director of the IEA.&lt;/em&gt; He puts it plainly: "there is no AI without energy" (&lt;a href="https://www.iea.org/news/data-centre-electricity-use-surged-in-2025-even-with-tightening-bottlenecks-driving-a-scramble-for-solutions" rel="noopener noreferrer"&gt;IEA, 2026&lt;/a&gt;). The IEA also says energy per AI task has fallen by at least an order of magnitude a year. Total demand still climbs, because there are far more calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  When: one check per stage
&lt;/h2&gt;

&lt;p&gt;Each stage gets one sustainability check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design.&lt;/strong&gt; Shortlist Regions on residency, cost and latency, then pick the lowest-carbon one left, as Well-Architected's SUS01-BP01 advises. Define your proxy KPI, and ask whether the feature needs an LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Default to arm64. Batch instead of trickling. Give every table, bucket and log group an expiry on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test.&lt;/strong&gt; Run Lambda Power Tuning, and record the proxy KPI next to p95.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate.&lt;/strong&gt; Read the proxy weekly and the carbon report monthly. Delete what's idle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where: your levers on AWS
&lt;/h2&gt;

&lt;p&gt;Well-Architected splits it. AWS handles sustainability &lt;em&gt;of&lt;/em&gt; the cloud: infrastructure, water, renewable power. You handle sustainability &lt;em&gt;in&lt;/em&gt; the cloud: your workloads, utilization and how much you deploy. Everything below is yours.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  AWS["AWS&amp;lt;br/&amp;gt;sustainability of the cloud"] --&amp;gt; A1["Infrastructure, water&amp;lt;br/&amp;gt;and renewable power"]
  YOU["You&amp;lt;br/&amp;gt;sustainability in the cloud"] --&amp;gt; Y1["Workloads, utilization&amp;lt;br/&amp;gt;and how much you deploy"]
  Y1 --&amp;gt; L1["Lambda on arm64"]
  Y1 --&amp;gt; L2["Graviton on EC2, ECS and EKS"]
  Y1 --&amp;gt; L3["DynamoDB on-demand with TTL"]
  Y1 --&amp;gt; L4["S3 lifecycle and Intelligent-Tiering"]
  Y1 --&amp;gt; L5["AWS Sustainability console and API"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;AWS owns the infrastructure, water and power, and every lever in this section sits on your side of the line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lambda on arm64.&lt;/strong&gt; For most Node.js and Python functions it's one line. AWS's 2021 launch claim was up to 19% better performance at 20% lower cost. Native binaries without arm64 builds will block you, and Compute Optimizer only sizes memory for x86_64 functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graviton on EC2, ECS and EKS.&lt;/strong&gt; AWS says Graviton instances use up to 60% less energy than comparable EC2 instances for the same performance. That's a vendor ceiling with no stated baseline, so benchmark your own workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DynamoDB on-demand with TTL.&lt;/strong&gt; On-demand removes idle capacity. TTL deletes expired items without using write throughput, typically within a few days, so filter them in reads. On global tables, replicated deletes do cost write units.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 lifecycle and Intelligent-Tiering.&lt;/strong&gt; Objects under 128 KB never tier down. AWS doesn't publish energy per storage class, so tiering is a proven cost lever but only an assumed carbon one. Expiration is the lever you can count on. Deleted bytes need no disks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AWS Sustainability console and API.&lt;/strong&gt; The Customer Carbon Footprint Tool added Scope 3 (hardware and buildings, fuel and energy, upstream transport) and Scope 1 data in October 2025. AWS deprecated it on 30 June 2026 for the AWS Sustainability console, which keeps the methodology but has its own permissions, so analysts don't need Billing access (&lt;a href="https://docs.aws.amazon.com/ccft/latest/releasenotes/what-is-ccftrn.html" rel="noopener noreferrer"&gt;CCFT release notes&lt;/a&gt;). The limits matter: monthly at best, published between the 15th and 21st of the next month, by account, Region and service only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source fills the gaps.&lt;/strong&gt; The Green Software Foundation's Carbon Aware SDK reads grid data from WattTime or Electricity Maps so you can shift work in time or place. Lambda Power Tuning finds the cheapest or fastest memory size. It measures money and time, not watts, so GB-seconds are your proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: which lever, and what it costs elsewhere
&lt;/h2&gt;

&lt;p&gt;I'd start with the certain, cheap levers and work down:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;Carbon effect&lt;/th&gt;
&lt;th&gt;Cost effect&lt;/th&gt;
&lt;th&gt;Main risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expire data, delete idle resources&lt;/td&gt;
&lt;td&gt;Large and certain&lt;/td&gt;
&lt;td&gt;Saves&lt;/td&gt;
&lt;td&gt;Deleting something you needed. Use TTL and lifecycle rules, not scripts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right-size memory and instances&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Saves&lt;/td&gt;
&lt;td&gt;Too small hurts p95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move to arm64 or Graviton&lt;/td&gt;
&lt;td&gt;Medium to large, per AWS&lt;/td&gt;
&lt;td&gt;Usually saves&lt;/td&gt;
&lt;td&gt;Dependencies without arm64 builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch and queue&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Saves&lt;/td&gt;
&lt;td&gt;Added latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run batch work in a low-carbon Region&lt;/td&gt;
&lt;td&gt;Large for that workload&lt;/td&gt;
&lt;td&gt;Neutral to higher&lt;/td&gt;
&lt;td&gt;Residency, more Regions to run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-shift to clean hours&lt;/td&gt;
&lt;td&gt;Small, a few percent per Hussain&lt;/td&gt;
&lt;td&gt;Neutral&lt;/td&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Region choice is where sustainability collides with two requirements at once. Picture a team serving users in Lagos. The closest AWS compute is the Lagos Local Zone, whose parent Region is Africa (Cape Town). Ember reports that South Africa generated 18% of its electricity from low-carbon sources in 2025, against a 43% global average. Sweden, home to Europe (Stockholm), managed 99%. And section 41 of Nigeria's Data Protection Act 2023 lets personal data leave the country only when the recipient offers adequate protection, unless another basis in the Act applies.&lt;/p&gt;

&lt;p&gt;Cleanest, closest and legal are three different answers.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  C["Cleanest grid&amp;lt;br/&amp;gt;Stockholm eu-north-1&amp;lt;br/&amp;gt;99% low-carbon in 2025"] ---|"long way from Lagos users"| L["Closest to users&amp;lt;br/&amp;gt;Lagos Local Zone&amp;lt;br/&amp;gt;parent Region Cape Town"]
  L ---|"you inherit an 18% low-carbon grid"| D["Where the law allows&amp;lt;br/&amp;gt;NDPA 2023 section 41&amp;lt;br/&amp;gt;adequacy test"]
  D ---|"may rule Stockholm out for personal data"| C
  B["Batch jobs, evals,&amp;lt;br/&amp;gt;fine-tuning"] -.-&amp;gt;|"free to move"| C
  I["Interactive APIs&amp;lt;br/&amp;gt;and chat"] -.-&amp;gt;|"stay close"| L
  P["Personal or&amp;lt;br/&amp;gt;regulated data"] -.-&amp;gt;|"stays put"| D&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Each workload picks its own corner: move what nobody waits for, and keep regulated and interactive paths where they must be.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So decide per workload. Here's how sustainability trades against the other eight requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NFR&lt;/th&gt;
&lt;th&gt;Where sustainability helps&lt;/th&gt;
&lt;th&gt;Where it hurts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Fewer resources, less attack surface&lt;/td&gt;
&lt;td&gt;Scanning, encryption and logging use compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;Data minimization serves both&lt;/td&gt;
&lt;td&gt;Residency can rule out the cleanest Regions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Lean pages work on low-end devices&lt;/td&gt;
&lt;td&gt;Rarely in conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Efficient code is usually faster&lt;/td&gt;
&lt;td&gt;Always-warm capacity burns energy idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Rarely helps&lt;/td&gt;
&lt;td&gt;Redundancy multiplies resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Scale-to-zero removes idle&lt;/td&gt;
&lt;td&gt;Headroom you never use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Fewer moving parts&lt;/td&gt;
&lt;td&gt;Carbon-driven multi-Region adds parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Mostly aligned&lt;/td&gt;
&lt;td&gt;A cheap token isn't always a low-energy token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hands-on: a lean ingest stack for Harbor Works
&lt;/h2&gt;

&lt;p&gt;Let's put the certain levers into code. Harbor Works is the fictional plant from Luso, this series' companion app, and its production lines send batches of inspection events. The stack uses an arm64 Lambda, on-demand DynamoDB with TTL, an S3 rule that tiers then expires raw batches, and two-week log retention.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the AWS CDK v2 API reference for &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html" rel="noopener noreferrer"&gt;lambda.Function&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.TableV2.html" rel="noopener noreferrer"&gt;dynamodb.TableV2&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.LifecycleRule.html" rel="noopener noreferrer"&gt;s3.LifecycleRule&lt;/a&gt; on Oct 29, 2026, type-checked in strict mode and synthesized with aws-cdk-lib 2.275.0. Not deployed, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CfnOutput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'constructs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib/aws-lambda'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib/aws-logs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib/aws-dynamodb'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib/aws-s3'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeanIngestStack&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nc"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Hot data. Inspection events expire through TTL, which uses no write throughput.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TableV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LineEvents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'lineId'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;sortKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDemand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// no idle provisioned capacity&lt;/span&gt;
      &lt;span class="n"&gt;timeToLiveAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'expiresAt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// epoch seconds, set by the writer&lt;/span&gt;
      &lt;span class="n"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// sandbox only, use RETAIN in prod&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Cold data. Raw batches tier down on their own, then get deleted.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'RawArchive'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;encryption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BucketEncryption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;S3_MANAGED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;blockPublicAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BlockPublicAccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BLOCK_ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;enforceSSL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;lifecycleRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'tier-then-expire'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'raw/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="c1"&gt;// Unknown access pattern: let Intelligent-Tiering decide.&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;storageClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StorageClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;INTELLIGENT_TIERING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transitionAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="c1"&gt;// After a year it's only kept for audit.&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;storageClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StorageClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DEEP_ARCHIVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transitionAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// the rule that actually removes bytes&lt;/span&gt;
        &lt;span class="n"&gt;abortIncompleteMultipartUploadAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;autoDeleteObjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// sandbox only&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Logs are data too. Two weeks, not the default of forever.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;logGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IngestLogs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RetentionDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TWO_WEEKS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ingest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ingest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;architecture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Architecture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ARM_64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Graviton&lt;/span&gt;
      &lt;span class="n"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// starting point, then Power Tuning decides&lt;/span&gt;
      &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'index.handler'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromInline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`
        exports.handler = async (event) =&amp;gt; {
          // Batch, don't trickle: one invocation handles many events.
          return { received: (event.Records || []).length };
        };`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;logGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;BUCKET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bucketName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantWriteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Printed by cdk deploy. Power Tuning needs it below.&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IngestArn'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functionArn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Pick the Region on residency and latency first. eu-north-1 is only an example.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LeanIngestStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LeanIngestStack'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'eu-north-1'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The synthesized template shows &lt;code&gt;Architectures: [arm64]&lt;/code&gt;, &lt;code&gt;BillingMode: PAY_PER_REQUEST&lt;/code&gt;, TTL, &lt;code&gt;RetentionInDays: 14&lt;/code&gt; and both transitions.&lt;/p&gt;

&lt;p&gt;So why 512 MB? It's a starting guess. Lambda gives CPU in proportion to memory, a full vCPU at 1,769 MB, so the setting with the fewest GB-seconds is sometimes larger than you'd expect. Deploy Power Tuning from its repo and let it decide.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://github.com/alexcasalboni/aws-lambda-power-tuning/blob/master/README-EXECUTE.md" rel="noopener noreferrer"&gt;Lambda Power Tuning execution docs&lt;/a&gt; on Oct 29, 2026. Not run, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# INGEST_ARN comes from the stack output. POWER_TUNING_ARN is the tool's state machine.&lt;/span&gt;
cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tuning-input.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;EOF
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"lambdaARN"&lt;/span&gt;: &lt;span class="s2"&gt;"${INGEST_ARN}"&lt;/span&gt;,
  &lt;span class="s2"&gt;"powerValues"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;128, 256, 512, 1024, 1769&lt;span class="o"&gt;]&lt;/span&gt;,
  &lt;span class="s2"&gt;"num"&lt;/span&gt;: 50,
  &lt;span class="s2"&gt;"payload"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Records"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"parallelInvocation"&lt;/span&gt;: true,
  &lt;span class="s2"&gt;"strategy"&lt;/span&gt;: &lt;span class="s2"&gt;"cost"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
EOF
aws stepfunctions start-execution \
  --state-machine-arn &lt;span class="s2"&gt;"$POWER_TUNING_ARN"&lt;/span&gt; \
  --input file://tuning-input.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then take a monthly carbon baseline. The end date is exclusive, so this returns January to September 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/sustainability/get-estimated-carbon-emissions.html" rel="noopener noreferrer"&gt;AWS CLI reference for get-estimated-carbon-emissions&lt;/a&gt; on Oct 29, 2026. Not run, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sustainability get-estimated-carbon-emissions \
  --time-period &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-01-01T00:00:00Z,End=2026-10-01T00:00:00Z \
  --group-by REGION SERVICE \
  --granularity MONTHLY \
  --emissions-types TOTAL_LBM_CARBON_EMISSIONS TOTAL_MBM_CARBON_EMISSIONS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chart the location-based column, the one Cockcroft says to tune on, next to your proxy KPI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sustainability in the age of AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI as a workload
&lt;/h3&gt;

&lt;p&gt;Patterson's and Wu's numbers agree: over a model's life, inference dominates, and inference is the part you pay for and control. The Green Software Foundation's &lt;strong&gt;SCI for AI&lt;/strong&gt;, ratified in December 2025, formalizes this. A provider boundary covers design, training and deployment. A consumer boundary covers operation, with units like carbon per token or per agent workflow.&lt;/p&gt;

&lt;p&gt;Better still, the Amazon Bedrock features that cut your token bill also cut compute per answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model Distillation.&lt;/strong&gt; AWS claims distilled models run up to 500% faster and cost 75% less, with under 2% accuracy loss for use cases like RAG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt caching.&lt;/strong&gt; Up to 90% lower cost and 85% lower latency, per AWS, because the cached prefix isn't reprocessed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Prompt Routing.&lt;/strong&gt; Picks between two models in a family per prompt, so easy requests can go to the smaller one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch inference and the Flex tier.&lt;/strong&gt; Batch costs 50% of on-demand for selected models. Flex is discounted for work that can wait.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the caveat. AWS publishes prices and speedups for these, not watts. The energy saving is my inference, not published data.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  F["Model Distillation&amp;lt;br/&amp;gt;prompt caching&amp;lt;br/&amp;gt;Intelligent Prompt Routing&amp;lt;br/&amp;gt;batch inference and Flex"] --&amp;gt;|"published by AWS"| C["Lower token bill&amp;lt;br/&amp;gt;and faster answers"]
  F -.-&amp;gt;|"inferred, no watts published"| W["Less compute&amp;lt;br/&amp;gt;per answer"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The cost saving is published by AWS, but the energy saving is an inference, not measured data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silicon.&lt;/strong&gt; AWS claims Inf2 instances give up to 50% better performance per watt than comparable EC2 instances. Trainium3's figures use different baselines, so they don't compare. The December 2025 launch post claims "4x greater energy efficiency" for UltraServers against Trainium2 UltraServers and "40% better" for the chip against earlier generations. Amazon's 2025 sustainability report says over five times more output tokens per megawatt. They matter if you self-host on SageMaker or EKS. On Bedrock, you don't pick the chip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The measurement gap.&lt;/strong&gt; The Sustainability API groups by &lt;code&gt;USAGE_ACCOUNT_ID&lt;/code&gt;, &lt;code&gt;REGION&lt;/code&gt; and &lt;code&gt;SERVICE&lt;/code&gt;, nothing finer. You'll see a monthly service share, not which model or agent produced it. So estimate per call from token counts and a public benchmark such as the AI Energy Score, label it an estimate, and track &lt;strong&gt;tokens per successful task&lt;/strong&gt; as your proxy. Cross-Region inference also moves prompts onto other grids, which makes it a Compliance decision too.&lt;/p&gt;

&lt;p&gt;The biggest lever comes first: whether to call a model at all. Luccioni's "orders of magnitude" is why this tree exists.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  A["New feature idea"] --&amp;gt; B{"Is the output fixed&amp;lt;br/&amp;gt;for a given input?"}
  B --&amp;gt;|"yes"| R1["Rules, regex, SQL&amp;lt;br/&amp;gt;or a lookup table"]
  B --&amp;gt;|"no"| C{"Narrow task with&amp;lt;br/&amp;gt;labeled examples?"}
  C --&amp;gt;|"yes"| R2["Small task-specific model&amp;lt;br/&amp;gt;classifier or extractor"]
  C --&amp;gt;|"no"| D{"Asked and answered&amp;lt;br/&amp;gt;before?"}
  D --&amp;gt;|"often"| R3["Cache or retrieve&amp;lt;br/&amp;gt;the stored answer"]
  D --&amp;gt;|"rarely"| E{"Does a user wait&amp;lt;br/&amp;gt;for the result?"}
  E --&amp;gt;|"no"| R4["Smallest adequate LLM&amp;lt;br/&amp;gt;through batch or Flex"]
  E --&amp;gt;|"yes"| R5["Smallest adequate LLM&amp;lt;br/&amp;gt;with caching and routing"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;A general-purpose LLM is the last stop on every branch, after rules, small models and caches.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At Harbor Works, flagging a reading above a line's temperature limit is a rule. Summarizing last night's shift log goes through batch, because nobody waits on it at 2 a.m.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;p&gt;Coding assistants are good at the mechanical work: porting builds to arm64, adding lifecycle rules and log retention across a repo. They're bad at numbers. Ask one for a Region's carbon intensity and you'll get a confident figure with no source. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept no carbon or energy figure from an assistant without a primary link.&lt;/li&gt;
&lt;li&gt;Judge each suggested optimization on the proxy KPI before and after, not on how good the explanation sounds.&lt;/li&gt;
&lt;li&gt;Count the assistant itself. Agent loops are inference too, so cap their iterations in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who: owners and the shared responsibility split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Product owner&lt;/th&gt;
&lt;th&gt;Architect&lt;/th&gt;
&lt;th&gt;Developers&lt;/th&gt;
&lt;th&gt;Platform / FinOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Define unit of work and proxy KPI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region and silicon choice&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right-sizing, arm64, retention&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly carbon reporting&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Should this use an LLM?"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;R = responsible, A = accountable, C = consulted, I = informed.&lt;/p&gt;

&lt;p&gt;Your share depends on the compute type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2.&lt;/strong&gt; AWS runs facilities, power and hardware. You own instance family, utilization, schedules and Region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fargate.&lt;/strong&gt; AWS also packs hosts. You own task size, arm64 images and scaling to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda.&lt;/strong&gt; AWS also packs environments. You own memory, architecture, invocations and logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock.&lt;/strong&gt; AWS runs serving and the model provider owns training. You own whether to call, which model, tokens, caching and batch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pick one unit of work and chart vCPU-minutes or GB-seconds per unit weekly.&lt;/li&gt;
&lt;li&gt;[ ] Record a location-based carbon baseline by Region and service.&lt;/li&gt;
&lt;li&gt;[ ] Switch your three busiest Lambda functions to &lt;code&gt;arm64&lt;/code&gt; and run Power Tuning on each.&lt;/li&gt;
&lt;li&gt;[ ] Give the five largest log groups a retention period and the five largest buckets a lifecycle rule.&lt;/li&gt;
&lt;li&gt;[ ] Add TTL to every DynamoDB table holding time-bound data.&lt;/li&gt;
&lt;li&gt;[ ] Check whether residency lets your batch and eval jobs run in a lower-carbon Region.&lt;/li&gt;
&lt;li&gt;[ ] Put every new AI feature through the decision tree, and log tokens per successful task.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I couldn't put these six in a room, but their published answers line up. Cockcroft gives the instruction: fewer computers, for less time. Hussain and the SCI make it measurable without offsets. Currie, Hsu and Bergman sort the work. Patterson and Luccioni show that for AI, inference and model choice are where your control sits. Birol warns that cheaper calls won't help if the calls keep multiplying.&lt;/p&gt;

&lt;p&gt;So keep three habits: a proxy you move weekly, a carbon report you read monthly, and the reflex of asking whether a model call is needed. Ireland's 23% is what millions of small provisioning decisions add up to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make every watt answer a question someone actually asked.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Series wrap-up: nine requirements, one system
&lt;/h2&gt;

&lt;p&gt;Part 1 started from Werner Vogels' re:Invent 2023 keynote and the first law of his Frugal Architect: make cost a non-functional requirement. Nine requirements later, none stands alone, and AI has pulled them tighter. One LLM feature now touches residency, prompt injection, screen readers on streamed text, time-to-first-token, throttling, token quotas, prompt versioning, the token bill and the energy behind it.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  SEC["Security"] --&amp;gt;|"controls add latency"| PERF["Performance"]
  SEC -.-&amp;gt;|"both non-negotiable"| COMP["Compliance"]
  COMP --&amp;gt;|"residency limits failover Regions"| AVAIL["Availability"]
  COMP --&amp;gt;|"residency limits clean Regions"| SUS["Sustainability"]
  A11Y["Accessibility"] -.-&amp;gt;|"lean pages help low-end devices"| PERF
  PERF --&amp;gt;|"warm capacity sits idle"| SUS
  AVAIL --&amp;gt;|"redundancy multiplies resources"| COST["Cost"]
  SCAL["Scalability"] --&amp;gt;|"headroom costs money"| COST
  SCAL -.-&amp;gt;|"autoscaling absorbs spikes"| AVAIL
  MAINT["Maintainability"] -.-&amp;gt;|"simple systems are easier to tune"| PERF
  COST -.-&amp;gt;|"mostly aligned"| SUS&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Solid arrows are tensions you have to decide, and dotted arrows are alliances you can use.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Security, Compliance and Accessibility are the floor. The other six are trade-offs you make on purpose, write down and revisit as things change.&lt;/p&gt;

&lt;p&gt;Now over to you. &lt;strong&gt;Which of the nine do you find hardest to get right, and why?&lt;/strong&gt; Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Availability&lt;/li&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;li&gt;Cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sustainability&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cso.ie/en/releasesandpublications/ep/p-dcmec/datacentresmeteredelectricityconsumption2025/keyfindings/" rel="noopener noreferrer"&gt;Data Centres Metered Electricity Consumption 2025 — Central Statistics Office Ireland, 7 July 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sustainability.aboutamazon.com/2025-amazon-sustainability-report.pdf" rel="noopener noreferrer"&gt;2025 Amazon Sustainability Report — Amazon, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sustainability.aboutamazon.com/2025-aws-summary.pdf" rel="noopener noreferrer"&gt;2025 AWS Summary (sustainability) — Amazon, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iea.org/reports/energy-and-ai" rel="noopener noreferrer"&gt;Energy and AI — IEA, April 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iea.org/spotlights/the-deepening-ties-between-energy-and-ai" rel="noopener noreferrer"&gt;The deepening ties between energy and AI — IEA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iea.org/reports/key-questions-on-energy-and-ai/executive-summary" rel="noopener noreferrer"&gt;Key Questions on Energy and AI, executive summary — IEA, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iea.org/news/data-centre-electricity-use-surged-in-2025-even-with-tightening-bottlenecks-driving-a-scramble-for-solutions" rel="noopener noreferrer"&gt;Data centre electricity use surged in 2025 — IEA, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://scientificamerican.com/article/ai-will-drive-doubling-of-data-center-energy-demand-by-2030" rel="noopener noreferrer"&gt;Data Centers Will Use Twice as Much Energy by 2030 — Scientific American, April 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/the-shared-responsibility-model.html" rel="noopener noreferrer"&gt;The shared responsibility model — AWS Well-Architected Sustainability Pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/evaluate-specific-improvements.html" rel="noopener noreferrer"&gt;Evaluate specific improvements (proxy metrics and KPIs) — AWS Well-Architected Sustainability Pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_region_a2.html" rel="noopener noreferrer"&gt;SUS01-BP01 Choose Region based on both business requirements and sustainability goals — AWS Well-Architected&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a5.html" rel="noopener noreferrer"&gt;SUS05-BP04 Optimize your use of hardware-based compute accelerators — AWS Well-Architected&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sci.greensoftware.foundation/" rel="noopener noreferrer"&gt;Software Carbon Intensity (SCI) Specification — Green Software Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://greensoftware.foundation/standards/sci-ai/" rel="noopener noreferrer"&gt;SCI for AI — Green Software Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://greensoftware.foundation/stories/sci-for-ai/" rel="noopener noreferrer"&gt;SCI for AI specification ratified — Green Software Foundation, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Green-Software-Foundation/sci-ai/blob/dev/SPEC.md" rel="noopener noreferrer"&gt;SCI for AI specification (SPEC.md) — Green Software Foundation, GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infoq.com/presentations/cloud-sustainability-green-energy" rel="noopener noreferrer"&gt;Cloud Provider Sustainability, Current Status and Future Directions — Adrian Cockcroft, QCon London 2023, InfoQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.aboutamazon.com/md/news/sustainability/cloud-computing-pioneers-new-focus-is-on-sustainability-transformation" rel="noopener noreferrer"&gt;Cloud computing pioneer's new focus is on sustainability transformation — Amazon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.technologyreview.com/2023/12/18/1085390/developing-climate-solutions-with-green-software/" rel="noopener noreferrer"&gt;Developing climate solutions with green software (Asim Hussain) — MIT Technology Review, December 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://greensoftware.foundation/articles/after-five-years-im-stepping-down-as-executive-director-of-the-green-software-fou/" rel="noopener noreferrer"&gt;After five years, I'm stepping down as Executive Director — Asim Hussain, Green Software Foundation, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.oreilly.com/library/view/building-green-software/9781098150617/" rel="noopener noreferrer"&gt;Building Green Software — Anne Currie, Sarah Hsu, Sara Bergman, O'Reilly, 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2204.05149" rel="noopener noreferrer"&gt;The Carbon Footprint of Machine Learning Training Will Plateau, Then Shrink — Patterson et al., 2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2111.00364" rel="noopener noreferrer"&gt;Sustainable AI: Environmental Implications, Challenges and Opportunities — Wu et al., 2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2508.15734" rel="noopener noreferrer"&gt;Measuring the environmental impact of delivering AI at Google Scale — Elsworth et al., August 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2311.16863" rel="noopener noreferrer"&gt;Power Hungry Processing: Watts Driving the Cost of AI Deployment? — Luccioni, Jernite, Strubell, ACM FAccT 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/blog/sasha/announcing-ai-energy-score" rel="noopener noreferrer"&gt;Announcing AI Energy Score Ratings — Sasha Luccioni, Hugging Face, February 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ccft/latest/releasenotes/what-is-ccftrn.html" rel="noopener noreferrer"&gt;Customer Carbon Footprint Tool release notes — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/10/aws-customer-carbon-footprint-tool-scope-3-emissions-data/" rel="noopener noreferrer"&gt;AWS Customer Carbon Footprint Tool now includes Scope 3 emissions data — AWS, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/announcing-the-aws-sustainability-console-programmatic-access-configurable-csv-reports-and-scope-1-3-reporting-in-one-place/" rel="noopener noreferrer"&gt;Announcing the AWS Sustainability console — AWS News Blog, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sustainability/latest/APIReference/API_GetEstimatedCarbonEmissions.html" rel="noopener noreferrer"&gt;GetEstimatedCarbonEmissions — AWS Sustainability API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/sustainability/get-estimated-carbon-emissions.html" rel="noopener noreferrer"&gt;get-estimated-carbon-emissions — AWS CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/graviton/graviton-sustainability/" rel="noopener noreferrer"&gt;AWS Graviton sustainability — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2021/09/better-price-performance-aws-lambda-functions-aws-graviton2-processor/" rel="noopener noreferrer"&gt;Up to 34% better price/performance for Lambda functions powered by Graviton2 — AWS, September 2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html" rel="noopener noreferrer"&gt;Lambda instruction set architectures — AWS Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html" rel="noopener noreferrer"&gt;Configure Lambda function memory — AWS Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html" rel="noopener noreferrer"&gt;Lambda runtimes — AWS Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/alexcasalboni/aws-lambda-power-tuning" rel="noopener noreferrer"&gt;AWS Lambda Power Tuning — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/alexcasalboni/aws-lambda-power-tuning/blob/master/README-EXECUTE.md" rel="noopener noreferrer"&gt;How to execute the state machine — AWS Lambda Power Tuning, GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sashaluccioni.com/about" rel="noopener noreferrer"&gt;About Sasha Luccioni — sashaluccioni.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ai/machine-learning/inferentia/" rel="noopener noreferrer"&gt;AWS Inferentia — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aboutamazon.com/news/aws/trainium-3-ultraserver-faster-ai-training-lower-cost" rel="noopener noreferrer"&gt;Trainium3 UltraServers — Amazon, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/05/amazon-bedrock-model-distillation-generally-available/" rel="noopener noreferrer"&gt;Amazon Bedrock Model Distillation is now generally available — AWS, May 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/04/amazon-bedrock-general-availability-prompt-caching/" rel="noopener noreferrer"&gt;Amazon Bedrock announces general availability of prompt caching — AWS, April 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/04/amazon-bedrock-intelligent-prompt-routing-generally-available/" rel="noopener noreferrer"&gt;Amazon Bedrock Intelligent Prompt Routing is now generally available — AWS, April 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-bedrock-fms-batch-inference-50-price/" rel="noopener noreferrer"&gt;Amazon Bedrock batch inference at 50% of on-demand price — AWS, August 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-bedrock-priority-flex-inference-service-tiers/" rel="noopener noreferrer"&gt;Amazon Bedrock introduces Priority and Flex inference service tiers — AWS, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html" rel="noopener noreferrer"&gt;Using time to live (TTL) in DynamoDB — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/intelligent-tiering-overview.html" rel="noopener noreferrer"&gt;How S3 Intelligent-Tiering works — S3 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://carbon-aware-sdk.greensoftware.foundation/docs/overview" rel="noopener noreferrer"&gt;Carbon Aware SDK overview — Green Software Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ember-energy.org/countries-and-regions/sweden/" rel="noopener noreferrer"&gt;Sweden — Ember&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ember-energy.org/countries-and-regions/south-africa/" rel="noopener noreferrer"&gt;South Africa — Ember&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/global-infrastructure/localzones/locations/" rel="noopener noreferrer"&gt;AWS Local Zones locations — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html" rel="noopener noreferrer"&gt;AWS Regions — AWS Global Infrastructure docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dataguidance.com/sites/default/files/data_protection_act_2023.pdf" rel="noopener noreferrer"&gt;Nigeria Data Protection Act 2023 (Part VIII, sections 41–43)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html" rel="noopener noreferrer"&gt;AWS CDK v2 API reference — lambda.Function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.TableV2.html" rel="noopener noreferrer"&gt;AWS CDK v2 API reference — dynamodb.TableV2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.LifecycleRule.html" rel="noopener noreferrer"&gt;AWS CDK v2 API reference — s3.LifecycleRule&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability" rel="noopener noreferrer"&gt;AWS service availability updates — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thefrugalarchitect.com/" rel="noopener noreferrer"&gt;The Frugal Architect — Werner Vogels&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>sustainability</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Cost</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 11 Nov 2026 15:03:02 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-cost-8h5</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-cost-8h5</guid>
      <description>&lt;p&gt;In January 2024, Jack Ellis, founder of the analytics company Fathom, published a line-by-line account of &lt;strong&gt;$94,885 a year&lt;/strong&gt; in savings on his team's AWS bill (&lt;a href="https://usefathom.com/blog/reduce-aws-bill" rel="noopener noreferrer"&gt;Fathom, 2024&lt;/a&gt;). Fathom has no outside funding, and its ingest endpoint takes billions of requests a month on Lambda.&lt;/p&gt;

&lt;p&gt;None of it came from a discount. It came from removing work: one Lambda invocation per pageview instead of two, a queue off the hot path, database traffic off a NAT gateway, logs nobody read.&lt;/p&gt;

&lt;p&gt;Here's the line that makes it a cost story. Fathom raised its entry-level price by only $1, Ellis wrote, "because we knew we could get our costs down and not have to charge our customers lots more."&lt;/p&gt;

&lt;p&gt;Cost decides what you can charge. That's why it belongs in your requirements.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make cost a number: &lt;strong&gt;cost per unit of value&lt;/strong&gt; (per request, tenant or successful task), not the monthly total.&lt;/li&gt;
&lt;li&gt;Most waste is multiplication. Find the path where the money goes and read it line by line.&lt;/li&gt;
&lt;li&gt;Run the FinOps loop, &lt;strong&gt;Inform → Optimize → Operate&lt;/strong&gt;, and tag first. You can't optimize spend nobody owns.&lt;/li&gt;
&lt;li&gt;Give every workload a budget and an anomaly monitor. It's about 90 lines of Terraform.&lt;/li&gt;
&lt;li&gt;For AI the unit is the &lt;strong&gt;token&lt;/strong&gt;, output costs about 5x input, and an agent without a session budget is an open tab.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What: cost, price and value
&lt;/h2&gt;

&lt;p&gt;People use these three interchangeably. Don't. &lt;strong&gt;Cost&lt;/strong&gt; is what you pay to deliver the service: the AWS bill, plus licenses, support and the people who run it. &lt;strong&gt;Price&lt;/strong&gt; is what your customer pays. &lt;strong&gt;Value&lt;/strong&gt; is what they get, and it caps the price. When cost per pageview drops, you can hold price and keep margin, which is exactly what Fathom did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj0rd1llzo3na357t6njk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj0rd1llzo3na357t6njk.png" alt="Cost, price and value. Cost: what you pay to deliver the service, meaning the AWS bill, licenses, support and the people who run it. Price: what your customer pays. Value: what your customer gets, and it caps the price. When cost per unit drops, you can hold price and keep your margin." width="799" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A bill that doubles while traffic triples is good news. The measurable form of this NFR is &lt;strong&gt;unit economics&lt;/strong&gt;: cost divided by a unit the business cares about. The FinOps Framework puts it in the &lt;em&gt;Quantify Business Value&lt;/em&gt; domain. AWS Well-Architected calls the same idea "measure overall efficiency."&lt;/p&gt;

&lt;p&gt;Pick units you can test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per request&lt;/strong&gt;, for APIs, and &lt;strong&gt;cost per tenant&lt;/strong&gt;, for SaaS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per successful task&lt;/strong&gt;, for AI features. Failed and retried calls count against it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag coverage&lt;/strong&gt;: the share of spend you can attribute to an owner. If much of the bill is untagged, every other number is a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write the target like a latency SLO: "ingest stays under $0.50 per million events at 2x today's traffic."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: waste is growing again
&lt;/h2&gt;

&lt;p&gt;Flexera's 2026 State of the Cloud survey of more than 750 cloud decision-makers put estimated wasted cloud spend at &lt;strong&gt;29%&lt;/strong&gt;, the first increase in five years, with AI workloads named as a driver. 85% called managing cloud spend their top challenge (&lt;a href="https://www.flexera.com/about-us/press-center/flexera-finds-cloud-value-is-rising-while-ai-waste-grows" rel="noopener noreferrer"&gt;Flexera, March 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;AI moved fast. In the State of FinOps 2026 (1,192 respondents), &lt;strong&gt;98%&lt;/strong&gt; said they now manage AI spend, up from 31% in 2024 (FinOps Foundation, 2026).&lt;/p&gt;

&lt;p&gt;Werner Vogels made it the first law of The Frugal Architect: "Make Cost a Non-functional Requirement." This series started from that keynote, so this part takes the law literally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case study: how Fathom cut $94,885 a year
&lt;/h2&gt;

&lt;p&gt;Fathom's ingest path looked like many serverless systems that grew fast. A third-party CDN sent each pageview through AWS WAF and an Application Load Balancer to a Lambda function, which put a job on SQS. A second Lambda wrote it to Redis and SingleStore and logged to CloudWatch. It ran on Laravel Vapor, which deploys Laravel to Lambda.&lt;/p&gt;

&lt;p&gt;The queue had a reason. Their database once ran in a single Availability Zone, so SQS buffered ingest from storage. That was an availability decision, and a sound one. Later the databases moved to multiple AZs. For a while, the queue stayed anyway.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph B["Before: every pageview"]
    B1["CDN"] --&amp;gt; B2["WAF + ALB"] --&amp;gt; B3["Lambda 1"] --&amp;gt; B4["SQS"] --&amp;gt; B5["Lambda 2"] --&amp;gt; B6["Redis + SingleStore"]
    B5 --&amp;gt; B7["CloudWatch logs&amp;lt;br/&amp;gt;nobody read"]
    B6 -.-&amp;gt;|"via NAT gateway"| B8["NAT data charges"]
  end
  subgraph A["After"]
    A1["CDN"] --&amp;gt; A2["WAF + ALB"] --&amp;gt; A3["One Lambda"] --&amp;gt; A6["Redis + SingleStore&amp;lt;br/&amp;gt;VPC peering, PrivateLink"]
    A3 -.-&amp;gt;|"only if the database is down"| A4["SQS fallback"]
  end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Each pageview went from two Lambda invocations and a queue hop to one invocation, with SQS kept only as a fallback.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ellis went after one endpoint, because "that's where all the money was being spent." What he found, in his numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQS, $23,989 a year, and Lambda, $20,862.&lt;/strong&gt; With multi-AZ databases, the hot path didn't need the queue. One Lambda request per pageview halved invocations, and duration fell because the function now did a Redis hit and an insert, each typically 1 ms or less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT gateway, $17,162.&lt;/strong&gt; Database traffic was crossing the NAT gateway. It moved to VPC peering for Redis and AWS PrivateLink for SingleStore. "After seeing vast amounts of data being moved across it, I admitted I was wrong."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch, $7,550.&lt;/strong&gt; Lambda wrote logs nobody used, since errors went to Sentry. He removed the log permission and told readers to set retention, which log groups lack by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3, $5,774.&lt;/strong&gt; Versioning was on in one bucket, quietly keeping history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rest:&lt;/strong&gt; WAF $12,201 (rate limiting moving to the CDN), CloudFront $4,800 (an internal sync moving about 4 TB a month), Route 53 $2,547 (longer DNS TTLs, fewer billed queries).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WAF and Route 53 were still in progress when he wrote it up, and CloudFront was a cost he said they "can easily remove", so about $19,500 of the total was still to come. Fathom doesn't publish its total bill, so there's no percentage, only dollars.&lt;/p&gt;

&lt;p&gt;Let's put a number on the multiplier. At us-east-1 list prices (October 2026), Lambda requests cost $0.20 per million and SQS standard requests $0.40 per million (AWS Price List). A billion events a month, each making one extra Lambda call and one queue request, add about $600 in request fees alone, before duration, logs or data transfer. Per event it's nothing. Every event, every month, it's a line item.&lt;/p&gt;

&lt;p&gt;Four lessons I'd put on any team's wall:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the path where the money goes.&lt;/strong&gt; Fathom audited one endpoint, the one carrying the traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count work per business event.&lt;/strong&gt; Two invocations and a queue hop per pageview is a design choice, and it multiplies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distrust defaults.&lt;/strong&gt; No log retention, versioning left on, traffic through NAT. Nobody decided any of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revisit a trade-off when its reason expires.&lt;/strong&gt; The queue paid for availability until multi-AZ databases took over that job.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The FinOps loop that makes it repeatable
&lt;/h2&gt;

&lt;p&gt;Fathom's review was a project. Keeping the savings takes a loop. The FinOps Framework calls its three phases &lt;strong&gt;Inform, Optimize and Operate&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  I["Inform&amp;lt;br/&amp;gt;tags, Cost Explorer,&amp;lt;br/&amp;gt;CUR 2.0, unit cost"] --&amp;gt; O["Optimize&amp;lt;br/&amp;gt;right-size, delete,&amp;lt;br/&amp;gt;Savings Plans"]
  O --&amp;gt; P["Operate&amp;lt;br/&amp;gt;budgets, anomaly alerts,&amp;lt;br/&amp;gt;monthly review"]
  P --&amp;gt;|"new usage, new questions"| I
  P -.-&amp;gt;|"cost in the design doc"| D["Next feature"]
  D -.-&amp;gt; I&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The loop only turns if Operate feeds back into Inform, and new features enter with a cost estimate already attached.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Duolingo ran this loop in 2024 and reported 20% annualized savings within months, with weekly cost reports, cost estimates in tech specs and quarterly spend reviews (&lt;a href="https://blog.duolingo.com/reducing-cloud-spending" rel="noopener noreferrer"&gt;Duolingo, 2024&lt;/a&gt;). One find rhymes with Fathom's: a staging environment cost more than production because someone scaled it up for a test and forgot.&lt;/p&gt;

&lt;h2&gt;
  
  
  When: where cost enters the lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design.&lt;/strong&gt; Estimate unit cost and count calls per business event before you build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Make the cheap path the default: log retention, S3 lifecycle rules, table TTLs, tags in IaC. Infracost (Apache 2.0) adds a cost diff to Terraform pull requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test.&lt;/strong&gt; Read cost per request off your load test, a day later, once Cost Explorer catches up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate.&lt;/strong&gt; Budgets and anomaly alerts per workload, a monthly look at Cost Optimization Hub, one owner per tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where: AWS services and the alternatives
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cost allocation tags&lt;/strong&gt; come first, because every other tool groups by them. A tag counts only after you activate it, and a backfill reaches back 12 months.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  T["Tag in IaC&amp;lt;br/&amp;gt;app, team, env"] --&amp;gt; R["Resources and&amp;lt;br/&amp;gt;Bedrock inference profiles"]
  R --&amp;gt; U["Usage records"]
  K["Activate tag&amp;lt;br/&amp;gt;in Billing console"] --&amp;gt; U
  U --&amp;gt; CE["Cost Explorer&amp;lt;br/&amp;gt;group by tag"]
  U --&amp;gt; CUR["Data Exports&amp;lt;br/&amp;gt;CUR 2.0"]
  U --&amp;gt; BU["AWS Budgets&amp;lt;br/&amp;gt;filter by tag"]
  U --&amp;gt; AD["Cost Anomaly Detection&amp;lt;br/&amp;gt;monitor by tag"]
  CUR --&amp;gt; UE["Unit cost&amp;lt;br/&amp;gt;per tenant or request"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;An untagged or unactivated resource never reaches the right-hand side, so it never gets an owner, a budget or an alert.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost Explorer&lt;/strong&gt; is the free console for trends: 13 months back, 18 months of forecast, refreshed at least daily. Its API costs $0.01 per paginated request. For unit cost, export line items with &lt;strong&gt;Data Exports (CUR 2.0)&lt;/strong&gt; and join them with request counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Budgets&lt;/strong&gt; alerts on actual or forecast spend and updates up to three times a day, so it's a smoke alarm, not a circuit breaker. Budget actions can apply an IAM policy or SCP, or stop EC2 and RDS instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost Anomaly Detection&lt;/strong&gt; runs about three times a day, can take 24 hours to spot something, and needs 10 days of history for a new service. Each account gets one AWS-managed services monitor, and a management account up to 500 customer-managed ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusted Advisor, Compute Optimizer and Cost Optimization Hub&lt;/strong&gt; find idle and oversized resources. The Hub prices them with your discounts. Trusted Advisor's full check set needs Business Support+ or higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source.&lt;/strong&gt; OpenCost, a CNCF incubating project, allocates Kubernetes cost down to the container. LiteLLM (MIT) enforces per-key and per-team budgets in an LLM gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: commitments, and what cost work costs elsewhere
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;th&gt;Up to&lt;/th&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Choose when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute Savings Plans&lt;/td&gt;
&lt;td&gt;EC2 any family or Region, Fargate, Lambda&lt;/td&gt;
&lt;td&gt;66% off&lt;/td&gt;
&lt;td&gt;1 or 3 years&lt;/td&gt;
&lt;td&gt;Your compute mix will change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 Instance Savings Plans&lt;/td&gt;
&lt;td&gt;One instance family in one Region&lt;/td&gt;
&lt;td&gt;72% off&lt;/td&gt;
&lt;td&gt;1 or 3 years&lt;/td&gt;
&lt;td&gt;A stable fleet you won't re-platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database Savings Plans&lt;/td&gt;
&lt;td&gt;Aurora, RDS, DynamoDB, ElastiCache, OpenSearch and more&lt;/td&gt;
&lt;td&gt;35% off&lt;/td&gt;
&lt;td&gt;1 year&lt;/td&gt;
&lt;td&gt;Steady database spend across engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserved Instances&lt;/td&gt;
&lt;td&gt;Specific instance types, Standard or Convertible&lt;/td&gt;
&lt;td&gt;72% off&lt;/td&gt;
&lt;td&gt;1 or 3 years&lt;/td&gt;
&lt;td&gt;Services or shapes Savings Plans don't fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot&lt;/td&gt;
&lt;td&gt;Spare EC2 capacity, interruptible&lt;/td&gt;
&lt;td&gt;90% off&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Stateless, retryable batch and CI work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock Reserved tier&lt;/td&gt;
&lt;td&gt;Guaranteed tokens per minute for a model&lt;/td&gt;
&lt;td&gt;Capacity, not a discount&lt;/td&gt;
&lt;td&gt;1 or 3 months&lt;/td&gt;
&lt;td&gt;Predictable, high-volume inference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AWS's maximum discounts, from the Savings Plans FAQ, Reserved Instances and Spot pages, October 2026. Commit to your floor, not your peak.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;How cost pulls against the other NFRs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NFR&lt;/th&gt;
&lt;th&gt;Where cost work helps it&lt;/th&gt;
&lt;th&gt;Where it hurts it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Deleting unused resources shrinks attack surface&lt;/td&gt;
&lt;td&gt;Cutting WAF or logs to save money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;Data minimization cuts storage and risk&lt;/td&gt;
&lt;td&gt;Residency can rule out the cheapest Region or global inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Lean pages suit cheap devices&lt;/td&gt;
&lt;td&gt;Generated captions and alt text cost model calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Fewer hops is faster too&lt;/td&gt;
&lt;td&gt;Caches, provisioned concurrency and Priority tier bill when idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Multi-AZ data can retire a buffer queue&lt;/td&gt;
&lt;td&gt;Every redundant copy is paid for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Scale-to-zero removes idle cost&lt;/td&gt;
&lt;td&gt;Commitments can lock in capacity you outgrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Fewer services to run&lt;/td&gt;
&lt;td&gt;Clever cost hacks are hard to hand over&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustainability&lt;/td&gt;
&lt;td&gt;Less compute per request saves energy too&lt;/td&gt;
&lt;td&gt;A cheap Region isn't always a clean one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hands-on: a budget, an anomaly monitor and a tagged model
&lt;/h2&gt;

&lt;p&gt;This stack gives one workload, the line copilot at Harbor Works (the fictional plant in Luso, this series' companion app), an owner tag, a tagged Bedrock inference profile, a monthly budget with forecast alerts and an anomaly monitor. Every alert goes to one SNS topic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/budgets_budget" rel="noopener noreferrer"&gt;Terraform AWS provider docs&lt;/a&gt; for each resource and the &lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ad-SNS.html" rel="noopener noreferrer"&gt;Cost Anomaly Detection SNS policy page&lt;/a&gt; on Oct 29, 2026. &lt;code&gt;terraform validate&lt;/code&gt; passes on Terraform 1.17.1 with AWS provider 6.71.0. Not deployed, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.71"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
  &lt;span class="n"&gt;default_tags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"line-copilot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# tag everything&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"me"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Activation fails until the key has appeared in billing data (up to 24 hours).&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ce_cost_allocation_tag"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;tag_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Active"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Use this profile's ARN as modelId so Bedrock token spend carries the tags.&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrock_inference_profile"&lt;/span&gt; &lt;span class="s2"&gt;"copilot"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"line-copilot-sonnet"&lt;/span&gt;
  &lt;span class="n"&gt;model_source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;copy_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:bedrock:us-east-1:${data.aws_caller_identity.me.account_id}:inference-profile/us.anthropic.claude-sonnet-4-6"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"shift-summary"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic"&lt;/span&gt; &lt;span class="s2"&gt;"cost"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"line-copilot-cost-alerts"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"cost"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;actions&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SNS:Publish"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="n"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"budgets.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"costalerts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceAccount"&lt;/span&gt;
      &lt;span class="n"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic_policy"&lt;/span&gt; &lt;span class="s2"&gt;"cost"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;arn&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;
  &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_budgets_budget"&lt;/span&gt; &lt;span class="s2"&gt;"copilot"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"line-copilot-monthly"&lt;/span&gt;
  &lt;span class="n"&gt;budget_type&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"COST"&lt;/span&gt;
  &lt;span class="n"&gt;limit_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1500"&lt;/span&gt;
  &lt;span class="n"&gt;limit_unit&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;
  &lt;span class="n"&gt;time_unit&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MONTHLY"&lt;/span&gt;
  &lt;span class="n"&gt;cost_filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TagKeyValue"&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user:app$line-copilot"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# format is user:&amp;lt;key&amp;gt;$&amp;lt;value&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;dynamic&lt;/span&gt; &lt;span class="s2"&gt;"notification"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;# warn on the forecast, then at 80% actual&lt;/span&gt;
    &lt;span class="n"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;FORECASTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ACTUAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;notification_type&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
      &lt;span class="n"&gt;threshold&lt;/span&gt;                 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
      &lt;span class="n"&gt;threshold_type&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PERCENTAGE"&lt;/span&gt;
      &lt;span class="n"&gt;comparison_operator&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GREATER_THAN"&lt;/span&gt;
      &lt;span class="n"&gt;subscriber_sns_topic_arns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;aws_sns_topic_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ce_anomaly_monitor"&lt;/span&gt; &lt;span class="s2"&gt;"copilot"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"line-copilot"&lt;/span&gt;
  &lt;span class="n"&gt;monitor_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CUSTOM"&lt;/span&gt; &lt;span class="c1"&gt;# customer managed: up to 10 tag values, monitored together&lt;/span&gt;
  &lt;span class="n"&gt;monitor_specification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;And&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CostCategories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Not&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Or&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="nc"&gt;Tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MatchOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"line-copilot"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ce_anomaly_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"copilot"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"line-copilot-immediate"&lt;/span&gt;
  &lt;span class="n"&gt;frequency&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"IMMEDIATE"&lt;/span&gt; &lt;span class="c1"&gt;# IMMEDIATE requires an SNS subscriber&lt;/span&gt;
  &lt;span class="n"&gt;monitor_arn_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;aws_ce_anomaly_monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copilot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;subscriber&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SNS"&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;threshold_expression&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dimension&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;key&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ANOMALY_TOTAL_IMPACT_ABSOLUTE"&lt;/span&gt;
      &lt;span class="n"&gt;match_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GREATER_THAN_OR_EQUAL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;values&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"50"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;aws_sns_topic_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things will catch you. Cost allocation tags are managed from the management account, so run the tag resource there. Tags aren't retroactive without a backfill. And a KMS-encrypted topic needs key policy grants for both services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost in the age of AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI as a workload: the token is the unit
&lt;/h3&gt;

&lt;p&gt;You pay for input and output tokens at different prices. In the us-east-1 price list dated October 12, 2026, Claude Sonnet 4.6 on the global cross-Region profile costs &lt;strong&gt;$3.00 per million input tokens and $15.00 per million output tokens&lt;/strong&gt; (&lt;a href="https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/us-east-1/index.json" rel="noopener noreferrer"&gt;AWS Price List&lt;/a&gt;). The US geographic profile costs 10% more.&lt;/p&gt;

&lt;p&gt;Take a Harbor Works shift summary: 6,000 tokens in, 500 out, &lt;strong&gt;$0.0255 per call&lt;/strong&gt;. Output is 8% of the tokens and 29% of the cost. A million summaries a month is $25,500, so cap &lt;code&gt;maxTokens&lt;/code&gt; and ask for short answers before anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribute it before you optimize it.&lt;/strong&gt; Application inference profiles, IAM principal tags (since April 2026), and projects and workspaces on the &lt;code&gt;bedrock-mantle&lt;/code&gt; endpoint all deliver billed dollars to Cost Explorer and CUR 2.0, per usage type per day (&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cost-management.html" rel="noopener noreferrer"&gt;Bedrock cost management&lt;/a&gt;). Profiles cover InvokeModel and Converse on &lt;code&gt;bedrock-runtime&lt;/code&gt;, one per model and tag set. Request metadata adds per-call detail to invocation logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert where the bill lands.&lt;/strong&gt; In May 2026, The Register reported a reader whose Claude Opus usage on Bedrock reached &lt;strong&gt;$30,141.33&lt;/strong&gt; in April with no anomaly alert (&lt;a href="https://www.theregister.com/saas/2026/05/14/bedrock-and-a-hard-place-claude-adventure-leaves-aws-user-staring-down-30k-invoice/5238153" rel="noopener noreferrer"&gt;The Register, 2026&lt;/a&gt;). Claude bills through AWS Marketplace, which anomaly detection didn't monitor then, and $8,026.54 of credits hid the spend for weeks. AWS added third-party Bedrock models on August 19, 2026. Keep a budget alarm too. A second alarm costs almost nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull the levers in order.&lt;/strong&gt; Each is cheaper to try than the next.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  L1["1. Right-size the model&amp;lt;br/&amp;gt;Haiku 4.5 is 3x cheaper than Sonnet 4.6 here,&amp;lt;br/&amp;gt;Nova 2 Lite about 8x"] --&amp;gt; L2["2. Intelligent Prompt Routing&amp;lt;br/&amp;gt;easy prompts go to the smaller model"]
  L2 --&amp;gt; L3["3. Prompt caching&amp;lt;br/&amp;gt;reuse the stable prefix"]
  L3 --&amp;gt; L4["4. Batch or Flex tier&amp;lt;br/&amp;gt;half price for work nobody waits on"]
  L4 --&amp;gt; L5["5. Distillation&amp;lt;br/&amp;gt;train a small model on the big one"]
  L5 --&amp;gt; L6["6. Reserved tier&amp;lt;br/&amp;gt;buy capacity once volume is steady"]
  E["Eval suite"] -.-&amp;gt;|"gates every step"| L1
  E -.-&amp;gt; L2
  E -.-&amp;gt; L5&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Start at the top and only move down when the step above is done and your evals still pass.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Right-size.&lt;/strong&gt; The same call costs $0.0085 on Claude Haiku 4.5 and $0.00305 on Amazon Nova 2 Lite, at global list prices. Run your evals on the smaller model first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Prompt Routing&lt;/strong&gt; sends each prompt to the cheaper model in a family unless the predicted quality gain clears your threshold. AWS claimed up to 30% savings at preview in 2024. It's optimized for English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt caching.&lt;/strong&gt; Care Access caches each medical record as the prompt prefix and changes only the question. It reported &lt;strong&gt;86% lower Bedrock costs&lt;/strong&gt; and 66% faster processing (&lt;a href="https://aws.amazon.com/blogs/machine-learning/how-care-access-achieved-86-data-processing-cost-reductions-and-66-faster-data-processing-with-amazon-bedrock-prompt-caching" rel="noopener noreferrer"&gt;AWS Machine Learning Blog, November 2025&lt;/a&gt;). On Sonnet 4.6 a cache read costs a tenth of fresh input, so a 4,000-token cached prefix cuts our call to $0.0147, once the first call has written it at a 25% premium. The prefix needs at least 1,024 tokens, and batch can't use the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch and Flex.&lt;/strong&gt; Batch lists at half the on-demand rate for these models. The Flex tier, set with &lt;code&gt;serviceTier&lt;/code&gt;, halves Nova 2 Lite's price but queues behind Standard under load. The 2 a.m. shift summary belongs here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distillation.&lt;/strong&gt; AWS claims distilled models run up to 500% faster and 75% cheaper with under 2% accuracy loss for RAG (AWS, May 2025). It's a vendor figure, so measure it on your own data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserved tier.&lt;/strong&gt; You buy tokens per minute for 1 or 3 months, with a 100,000 input TPM minimum. In AWS's October 2026 price list, a fully used 1-month Sonnet 4.6 reservation costs the same per token as on-demand Standard, and the 3-month term is 10% less. The pricing page doesn't show Reserved rates and access goes through your account team, so confirm your quote. You're buying guaranteed capacity, not a discount.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Self-hosting is a seventh lever that pays only at volume. Part 7, Scalability, works through that break-even.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runaway agents are a new failure mode.&lt;/strong&gt; One request can become dozens of model calls, and a loop that never reaches &lt;code&gt;end_turn&lt;/code&gt; burns tokens until something stops it. Billing alarms see it hours later. The stop belongs in the request path.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant U as User
  participant A as Agent loop
  participant G as Budget guard
  participant K as Kill switch
  participant B as Bedrock
  U-&amp;gt;&amp;gt;A: one request
  loop each step
    A-&amp;gt;&amp;gt;G: next call
    G-&amp;gt;&amp;gt;K: AI enabled? cached 30s
    G-&amp;gt;&amp;gt;G: steps and dollars under cap?
    G-&amp;gt;&amp;gt;B: Converse with maxTokens sized to what is left
    B--&amp;gt;&amp;gt;G: usage in tokens
    G-&amp;gt;&amp;gt;G: add cost to session total
  end
  G--&amp;gt;&amp;gt;A: BudgetExceeded
  A--&amp;gt;&amp;gt;U: partial answer and a clear message&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The guard checks the switch and the session's spend before every call, so a runaway loop stops within one step, not one billing cycle.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Converse API reference&lt;/a&gt; and the us-east-1 Bedrock price list on Oct 29, 2026. Unit-tested with botocore's Stubber on boto3 1.43.121 for the budget-exceeded and kill-switch paths. Not deployed, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# budget_guard.py: per-session token budget and a kill switch around Converse&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-runtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ssm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ssm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# USD per 1M tokens: Claude Sonnet 4.6, global profile, us-east-1 price list, Oct 2026&lt;/span&gt;
&lt;span class="no"&gt;PRICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"out"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cache_read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cache_write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.75&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;_switch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"on"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


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


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ai_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""One SSM parameter flips every agent off. Cached so each step isn't an SSM call."""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_switch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/line-copilot/ai-enabled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Parameter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;_switch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_switch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"on"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"inputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;PRICE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"outputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;PRICE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"out"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cacheReadInputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;PRICE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cache_read"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cacheWriteInputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;PRICE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cache_write"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;ai_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;BudgetExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"kill switch is off"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;BudgetExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{self.id}: {self.steps} steps, ${self.spent:.4f}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Cap output to what the remaining budget can pay for (floor of 64 tokens).&lt;/span&gt;
        &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spent&lt;/span&gt;
        &lt;span class="n"&gt;max_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="no"&gt;PRICE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"out"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;inferenceConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_out&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;requestMetadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# per-call detail in invocation logs&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;call_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass the profile ARN from the Terraform stack as &lt;code&gt;model_id&lt;/code&gt; and the spend lands under the &lt;code&gt;app&lt;/code&gt; tag too. The guard can overshoot by one call, so the output cap shrinks as the budget runs down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector storage can quietly dominate RAG cost.&lt;/strong&gt; S3 Vectors charges $0.06 per GB-month, so a million 1,024-dimension vectors, about 4 GB, cost roughly $0.25 a month to store, plus $2.50 per million queries (&lt;a href="https://aws.amazon.com/s3/pricing/" rel="noopener noreferrer"&gt;S3 pricing&lt;/a&gt;). A classic OpenSearch Serverless collection bills at least 2 OCUs, about $350 a month at $0.24 per OCU-hour. The catch is S3 Vectors' latency, around 100 ms for frequent queries, so put it behind a smaller, faster hot tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;p&gt;AI assistants are good at the reading. The AWS Billing and Cost Management MCP server lets an assistant query cost, budget, anomaly and Savings Plans data, with a SQL engine for the arithmetic (AWS, August 2025). The AWS FinOps Agent, in free preview in us-east-1 since June 9, 2026, investigates anomalies and can open Jira tickets and post to Slack (AWS, June 2026).&lt;/p&gt;

&lt;p&gt;They're bad at judgment. An assistant will happily recommend a three-year commitment for a workload you're about to re-platform. The guardrails I'd set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only billing roles. No purchase or budget-action permissions.&lt;/li&gt;
&lt;li&gt;Every number cites a query you can rerun. Spot-check it against CUR.&lt;/li&gt;
&lt;li&gt;Count the assistant's own cost: Cost Explorer API pages are $0.01 each, and agent loops are inference too.&lt;/li&gt;
&lt;li&gt;A human approves every commitment, deletion and budget action.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who: owners and the shared responsibility split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Product owner&lt;/th&gt;
&lt;th&gt;Architect&lt;/th&gt;
&lt;th&gt;Developers&lt;/th&gt;
&lt;th&gt;Platform / FinOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit cost target and price decisions&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calls per event, model choice, cost in design docs&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags, retention, caps, budget guards&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budgets, anomaly alerts, commitments, monthly review&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;R = responsible, A = accountable, C = consulted, I = informed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS sets prices, meters usage and supplies recommendations. What you control depends on compute type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2&lt;/strong&gt;: instance size, purchase option, and shutting down what's idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers (ECS on Fargate, EKS)&lt;/strong&gt;: task and pod sizing, bin packing, the Spot mix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: memory, duration, invocations per event, log volume. INIT time has been billed since August 1, 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed AI (Bedrock)&lt;/strong&gt;: the provider sets token prices and caching rules. You own tokens per task, model choice, output caps and session budgets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pick one unit cost metric for your busiest workload and write a target for it.&lt;/li&gt;
&lt;li&gt;[ ] Find the endpoint with the most spend and count service calls per request on it.&lt;/li&gt;
&lt;li&gt;[ ] Set retention on every CloudWatch log group that has none.&lt;/li&gt;
&lt;li&gt;[ ] Activate &lt;code&gt;app&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt; as cost allocation tags and check tag coverage.&lt;/li&gt;
&lt;li&gt;[ ] Deploy the budget and anomaly monitor above for one workload.&lt;/li&gt;
&lt;li&gt;[ ] Route Bedrock calls through a tagged application inference profile.&lt;/li&gt;
&lt;li&gt;[ ] Cap &lt;code&gt;maxTokens&lt;/code&gt; on every model call, and put a per-session budget on every agent.&lt;/li&gt;
&lt;li&gt;[ ] Run your eval set on the next-smaller model.&lt;/li&gt;
&lt;li&gt;[ ] Add a prompt cache checkpoint after your stable prefix and watch &lt;code&gt;cacheReadInputTokens&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Fathom didn't negotiate a better rate. It read its ingest path one service at a time and removed work each request didn't need: a second invocation, a queue that had outlived its reason, traffic through NAT, logs nobody opened. That bought the room to raise prices by a dollar instead of more.&lt;/p&gt;

&lt;p&gt;The method holds for AI. Measure the unit, tokens per successful task. Attribute it with tags, profiles and principals. Pull the levers in order and gate each with evals. Then put a budget in the request path, because the billing alarm arrives a day late.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your bill is your architecture, itemized.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Availability&lt;/li&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Sustainability (coming soon)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up, Sustainability:&lt;/strong&gt; the series closes with published expert voices on energy and carbon per unit of work, and why most cost levers are carbon levers too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://usefathom.com/blog/reduce-aws-bill" rel="noopener noreferrer"&gt;Reducing our AWS bill by $100,000 — Jack Ellis, Fathom Analytics, January 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buzzsprout.com/877747/14535024-96-how-fathom-saved-100k-year-in-their-aws-bill" rel="noopener noreferrer"&gt;How Fathom saved $100k/year in their AWS bill — Real World Serverless with Yan Cui, February 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.duolingo.com/reducing-cloud-spending" rel="noopener noreferrer"&gt;How we reduced our cloud spending by 20% — Duolingo, October 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.flexera.com/about-us/press-center/flexera-finds-cloud-value-is-rising-while-ai-waste-grows" rel="noopener noreferrer"&gt;Flexera finds cloud value is rising while AI waste grows — Flexera, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://data.finops.org/" rel="noopener noreferrer"&gt;State of FinOps 2026 — FinOps Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.finops.org/framework/" rel="noopener noreferrer"&gt;FinOps Framework — FinOps Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.finops.org/framework/capabilities/unit-economics/" rel="noopener noreferrer"&gt;Unit Economics capability — FinOps Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thefrugalarchitect.com/" rel="noopener noreferrer"&gt;The Frugal Architect — Werner Vogels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/cost-optimization-pillar/design-principles.html" rel="noopener noreferrer"&gt;Cost Optimization Pillar: design principles — AWS Well-Architected&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json" rel="noopener noreferrer"&gt;AWS Price List API: Lambda, SQS and Amazon Bedrock offer files for us-east-1, October 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/us-east-1/index.json" rel="noopener noreferrer"&gt;Amazon Bedrock foundation model offer file, us-east-1, October 12, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ce-what-is.html" rel="noopener noreferrer"&gt;What is AWS Cost Explorer? — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cur/latest/userguide/what-is-data-exports.html" rel="noopener noreferrer"&gt;What is AWS Data Exports? — Data Exports docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/cost-alloc-tags.html" rel="noopener noreferrer"&gt;Using cost allocation tags — AWS Billing docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/cost-allocation-backfill.html" rel="noopener noreferrer"&gt;Backfill cost allocation tags — AWS Billing docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html" rel="noopener noreferrer"&gt;Managing your costs with AWS Budgets — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-controls.html" rel="noopener noreferrer"&gt;Configuring budget actions — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-create-filters.html" rel="noopener noreferrer"&gt;Budget filters — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-sns-policy.html" rel="noopener noreferrer"&gt;AWS Budgets SNS topic policy — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/manage-ad.html" rel="noopener noreferrer"&gt;Detecting unusual spend with AWS Cost Anomaly Detection — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/getting-started-ad.html" rel="noopener noreferrer"&gt;Getting started with AWS Cost Anomaly Detection — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ad-SNS.html" rel="noopener noreferrer"&gt;Creating an Amazon SNS topic for anomaly notifications — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/management-limits.html" rel="noopener noreferrer"&gt;Quotas and restrictions — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/cost-optimization-hub.html" rel="noopener noreferrer"&gt;Cost Optimization Hub — AWS Cost Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/compute-optimizer/latest/ug/what-is-compute-optimizer.html" rel="noopener noreferrer"&gt;What is AWS Compute Optimizer? — Compute Optimizer docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/awssupport/latest/user/trusted-advisor.html" rel="noopener noreferrer"&gt;AWS Trusted Advisor — AWS Support docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/savingsplans/faq/" rel="noopener noreferrer"&gt;Savings Plans FAQ — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/database-savings-plans-savings" rel="noopener noreferrer"&gt;Announcing Database Savings Plans with up to 35% savings — AWS, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/pricing/reserved-instances/" rel="noopener noreferrer"&gt;Amazon EC2 Reserved Instances pricing — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/spot/" rel="noopener noreferrer"&gt;Amazon EC2 Spot Instances — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/compute/aws-lambda-standardizes-billing-for-init-phase/" rel="noopener noreferrer"&gt;AWS Lambda standardizes billing for the INIT phase — AWS Compute Blog, 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability" rel="noopener noreferrer"&gt;AWS service availability updates — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/infracost/infracost" rel="noopener noreferrer"&gt;Infracost — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.opencost.io/" rel="noopener noreferrer"&gt;OpenCost — CNCF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.litellm.ai/docs/proxy/users" rel="noopener noreferrer"&gt;LiteLLM budgets and rate limits — LiteLLM docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/budgets_budget" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_budgets_budget&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_anomaly_monitor" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_ce_anomaly_monitor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_anomaly_subscription" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_ce_anomaly_subscription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_cost_allocation_tag" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_ce_cost_allocation_tag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/bedrock_inference_profile" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_bedrock_inference_profile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cost-management.html" rel="noopener noreferrer"&gt;Track usage and costs in Amazon Bedrock — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cost-mgmt-application-inference-profiles.html" rel="noopener noreferrer"&gt;Application inference profiles — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-create.html" rel="noopener noreferrer"&gt;Create an application inference profile — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/04/bedrock-iam-cost-allocation" rel="noopener noreferrer"&gt;Amazon Bedrock now supports cost allocation by IAM user and role — AWS, April 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws-cloud-financial-management/track-amazon-bedrock-costs-by-caller-identity-with-iam-based-cost-allocation/" rel="noopener noreferrer"&gt;Track Amazon Bedrock costs by caller identity with IAM-based cost allocation — AWS Cloud Financial Management Blog, April 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/01/granular-amazon-bedrock" rel="noopener noreferrer"&gt;AWS Data Exports adds granular operation visibility for Amazon Bedrock — AWS, January 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/aws-bedrock-attributes-in-cost-explorer/" rel="noopener noreferrer"&gt;Amazon Bedrock product attributes in Cost Explorer — AWS, October 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/08/aws-cost-anomaly-detection-bedrock-3P/" rel="noopener noreferrer"&gt;AWS Cost Anomaly Detection now monitors third-party models on Amazon Bedrock — AWS, August 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/saas/2026/05/14/bedrock-and-a-hard-place-claude-adventure-leaves-aws-user-staring-down-30k-invoice/5238153" rel="noopener noreferrer"&gt;Bedrock and a hard place: Claude adventure leaves AWS user staring down $30K invoice — The Register, May 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html" rel="noopener noreferrer"&gt;Prompt caching for faster model inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/how-care-access-achieved-86-data-processing-cost-reductions-and-66-faster-data-processing-with-amazon-bedrock-prompt-caching" rel="noopener noreferrer"&gt;How Care Access achieved 86% data processing cost reductions with Amazon Bedrock prompt caching — AWS Machine Learning Blog, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-routing.html" rel="noopener noreferrer"&gt;Intelligent prompt routing — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/reduce-costs-and-latency-with-amazon-bedrock-intelligent-prompt-routing-and-prompt-caching-preview/" rel="noopener noreferrer"&gt;Reduce costs and latency with Intelligent Prompt Routing and prompt caching (preview) — AWS News Blog, December 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/05/amazon-bedrock-model-distillation-generally-available/" rel="noopener noreferrer"&gt;Amazon Bedrock Model Distillation is generally available — AWS, May 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html" rel="noopener noreferrer"&gt;Service tiers for inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-bedrock-priority-flex-inference-service-tiers/" rel="noopener noreferrer"&gt;Amazon Bedrock introduces Priority and Flex inference service tiers — AWS, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-bedrock-reserved-service-tier" rel="noopener noreferrer"&gt;Amazon Bedrock introduces Reserved service tier — AWS, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-4-6.html" rel="noopener noreferrer"&gt;Claude Sonnet 4.6 model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html" rel="noopener noreferrer"&gt;Amazon Nova 2 Lite model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Converse — Amazon Bedrock API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-s3-vectors-generally-available/" rel="noopener noreferrer"&gt;Amazon S3 Vectors is generally available — AWS, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/pricing/" rel="noopener noreferrer"&gt;Amazon S3 pricing (S3 Vectors) — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/opensearch-service/pricing/" rel="noopener noreferrer"&gt;Amazon OpenSearch Service pricing — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/08/aws-billing-cost-management-mcp-server" rel="noopener noreferrer"&gt;Announcing the AWS Billing and Cost Management MCP server — AWS, August 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/aws-finops-agent-preview/" rel="noopener noreferrer"&gt;AWS FinOps Agent is now available in preview — AWS, June 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>finops</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Maintainability</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 11 Nov 2026 15:02:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-maintainability-531b</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-maintainability-531b</guid>
      <description>&lt;p&gt;On August 1, 2012, Knight Capital rolled out new code to its order router by hand and missed one of the eight servers. The new code also reused a flag that once switched on "Power Peg", a feature nobody had used since 2003. On the missed server, that flag woke the dead code up. In 45 minutes the router sent millions of orders and got more than 4 million executions while trying to fill 212 customer orders, and Knight lost more than $460 million. The SEC found that no second technician reviewed the deploy, and no written procedure required one (&lt;a href="https://www.sec.gov/litigation/admin/2013/34-70694.pdf" rel="noopener noreferrer"&gt;SEC order, 2013&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Nothing in that story is exotic. It took dead code, a flag with two meanings and a deploy that depended on nobody missing a step.&lt;/p&gt;

&lt;p&gt;That's what maintainability failures usually look like. The system runs fine until somebody has to change it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintainability is how cheaply and safely you can change a running system. Measure it with DORA's five delivery metrics.&lt;/li&gt;
&lt;li&gt;Prompts and model IDs are code, model end-of-life is a scheduled outage, and MCP servers are dependencies. Version, test and inventory all three.&lt;/li&gt;
&lt;li&gt;Gate every prompt or model change on an eval suite: a golden set, cheap checks, then a calibrated judge.&lt;/li&gt;
&lt;li&gt;AI raises throughput but, per DORA 2025, still hurts stability. Small batches and fast rollback turn speed into delivery.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What: maintainability, and how to measure it
&lt;/h2&gt;

&lt;p&gt;ISO/IEC 25010:2023 defines maintainability as the "capability of a product to be modified by the intended maintainers with effectiveness and efficiency." For a cloud system, I break that into five parts you can check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modifiability.&lt;/strong&gt; You change one thing without touching five others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability.&lt;/strong&gt; A machine tells you in minutes whether the change broke anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability.&lt;/strong&gt; You can follow one request end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployability.&lt;/strong&gt; You can ship and undo a change without a hero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operability.&lt;/strong&gt; On-call can run it from a runbook, upgrades included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xtd6pb19zij6on6yvnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xtd6pb19zij6on6yvnh.png" alt="Five parts of maintainability you can check, as cards: modifiability, testability, observability, deployability and operability, each with the one-line test from the list above." width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clean code helps, but a tidy codebase with a manual deploy and no rollback is still hard to maintain.&lt;/p&gt;

&lt;p&gt;The measurable form is DORA's delivery metrics, five since 2024. &lt;strong&gt;Throughput&lt;/strong&gt; is change lead time, deployment frequency and failed deployment recovery time. &lt;strong&gt;Instability&lt;/strong&gt; is change fail rate and deployment rework rate, the share of deploys forced by a production incident. DORA says to measure one service at a time and never turn the numbers into targets. For AI features, add the eval pass rate per prompt version and the days until your nearest model end-of-life.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: neglect compounds
&lt;/h2&gt;

&lt;p&gt;Unmaintainable code costs time. Adam Tornhill and Markus Borg found that low-quality code had 15 times more defects than high-quality code, and that fixing issues in it took 124% more development time (&lt;a href="https://arxiv.org/abs/2203.04374" rel="noopener noreferrer"&gt;Code Red, 2022&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;AI made the problem faster, not smaller. DORA's 2024 report estimated that a 25% rise in AI adoption went with a 7.2% drop in delivery stability. The 2025 edition, the latest at the time of writing, surveyed nearly 5,000 professionals. 90% use AI at work, and AI adoption now goes with higher throughput but still with lower stability (&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report" rel="noopener noreferrer"&gt;DORA, 2025&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Don't trust how fast it feels, either. In METR's 2025 trial, experienced open-source developers took 19% longer with AI tools while believing they'd been 20% faster (&lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;METR, 2025&lt;/a&gt;). METR called the data from its late-2025 follow-up, published in February 2026, "an unreliable signal", so treat the 19% as a warning about perception.&lt;/p&gt;

&lt;h2&gt;
  
  
  When: where maintainability enters the lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design.&lt;/strong&gt; Give every prompt, model ID and flag one owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Everything deployable lives in Git, prompts and IaC included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test.&lt;/strong&gt; Evals on every prompt or model change, a canary on every deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate.&lt;/strong&gt; Drift checks, a lifecycle inventory, traces you can follow, and a monthly look at the DORA metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where: two pitfalls that predate AI
&lt;/h2&gt;

&lt;p&gt;Each pitfall follows the same arc: what you'll notice, why it happens, the fix, and the tool I'd reach for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjynexgizx87551puc0op.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjynexgizx87551puc0op.png" alt="Anatomy of a pitfall, as four steps joined by arrows. Symptom: a deploy only one person can run, or answers that got worse with an empty diff. Cause: a step nobody wrote down, or something that changes outside version control. Fix: make the change visible, tested and reversible. Tool: a pipeline, a drift check, an eval gate or a pinned dependency." width="799" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 1: Deploys that live in someone's head
&lt;/h3&gt;

&lt;p&gt;You'll notice it as a deploy that only works when one person runs it, and a rollback nobody wants to try.&lt;/p&gt;

&lt;p&gt;It happens because the deploy started as a script for one server and grew. Nobody wrote every step down, and a missed step fails silently, as it did at Knight.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A["Manual deploy&amp;lt;br/&amp;gt;to 8 servers"] --&amp;gt; B["1 server missed"]
  C["New code reuses&amp;lt;br/&amp;gt;an old flag"] --&amp;gt; D["Flag switched on&amp;lt;br/&amp;gt;at market open"]
  B --&amp;gt; D
  D --&amp;gt;|"on the missed server"| E["Power Peg code&amp;lt;br/&amp;gt;unused since 2003 runs"]
  E --&amp;gt; F["4 million executions in 45 min&amp;lt;br/&amp;gt;loss over 460M USD"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;It took three ordinary gaps lining up: a manual deploy, dead code left in place, and a flag that meant two things.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is a pipeline that is the only road to production, with rollback that needs no human. CodePipeline orchestrates, CodeBuild tests, and CodeDeploy shifts traffic. For Lambda, &lt;code&gt;CodeDeployDefault.LambdaCanary10Percent5Minutes&lt;/code&gt; sends 10% of traffic to the new version, waits five minutes, then shifts the rest. Add a CloudWatch alarm to the deployment group with auto-rollback on &lt;code&gt;DEPLOYMENT_STOP_ON_ALARM&lt;/code&gt;, and a bad canary redeploys the last good revision while you sleep. CDK Pipelines updates its own stages from the repo once deployed. GitHub Actions works too, and CodeCommit has been generally available again since November 2025.&lt;/p&gt;

&lt;p&gt;Content needs staged rollouts too. CrowdStrike's July 2024 outage came from a content update, and its root-cause fixes include staged rings with bake time. Prompts and feature flags are content. Keep flags in AWS AppConfig, and never give an old flag a new meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 2: The console fix that never made it back
&lt;/h3&gt;

&lt;p&gt;You'll notice it when the next deploy undoes last month's console hotfix, or fails on a resource that "shouldn't" look like that.&lt;/p&gt;

&lt;p&gt;It happens because incidents reward speed, and nobody owns the follow-up.&lt;/p&gt;

&lt;p&gt;The fix is to check for drift on a schedule and before every deploy, then fix it in code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cdk drift MyStack --fail&lt;/code&gt; exits with code 1 on drift, so it works as a pipeline step.&lt;/li&gt;
&lt;li&gt;The AWS Config rule &lt;code&gt;cloudformation-stack-drift-detection-check&lt;/code&gt; runs periodically, within a 15-minute limit, so scope it with tags.&lt;/li&gt;
&lt;li&gt;Drift-aware change sets (November 2025) with &lt;code&gt;--deployment-mode REVERT_DRIFT&lt;/code&gt; put drifted resources back to the template.&lt;/li&gt;
&lt;li&gt;In Terraform, &lt;code&gt;terraform plan -refresh-only -detailed-exitcode&lt;/code&gt; returns 2 when something changed outside Terraform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudFormation only checks properties you set explicitly, so write the ones that matter into the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintainability in the age of AI
&lt;/h2&gt;

&lt;p&gt;AI changes maintainability from two sides. As a workload, it adds things that change without a code diff. As an engineering aid, it writes more code than your review process was built for.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as a workload
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Pitfall 3: Prompts and model IDs outside version control
&lt;/h4&gt;

&lt;p&gt;You'll notice it when answers got worse on Tuesday and the diff is empty. The prompt lives in a console text box or an environment variable, and the model ID is a string someone edited.&lt;/p&gt;

&lt;p&gt;It happens because prompts feel like copy, so they get edited like copy. But a prompt plus a model ID &lt;em&gt;is&lt;/em&gt; your feature's behavior, so changing either is a release.&lt;/p&gt;

&lt;p&gt;The fix is to keep both in Git and ship them through the pipeline. Amazon Bedrock Prompt Management stores prompts as numbered snapshot versions, so the app calls version 7 while you edit the draft. When you pass a prompt version ARN to Converse as the &lt;code&gt;modelId&lt;/code&gt;, the request can't include &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;inferenceConfig&lt;/code&gt;, &lt;code&gt;toolConfig&lt;/code&gt; or &lt;code&gt;additionalModelRequestFields&lt;/code&gt; (&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Converse API reference&lt;/a&gt;). That's the point. The version owns the model and its settings, not the caller.&lt;/p&gt;

&lt;p&gt;Here's the shift-assistant prompt for Harbor Works, the fictional plant in my companion app Luso, as CDK. The template text sits in a file beside the code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrock.CfnPrompt.html" rel="noopener noreferrer"&gt;CDK &lt;code&gt;CfnPrompt&lt;/code&gt; reference&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-bedrock-promptversion.html" rel="noopener noreferrer"&gt;AWS::Bedrock::PromptVersion&lt;/a&gt; on Oct 29, 2026. Type-checked and synthesized with aws-cdk-lib 2.275.0 and TypeScript 5.9.3. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'node:crypto'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'node:fs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StackProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib/aws-bedrock'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ssm&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'aws-cdk-lib/aws-ssm'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'constructs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Prompt text, model ID and settings live in Git, next to the code that calls them.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PromptVariantProperty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;templateType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'TEXT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'eu.anthropic.claude-sonnet-4-6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// pinned EU geo inference profile&lt;/span&gt;
  &lt;span class="n"&gt;inferenceConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;templateConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'prompts/shift-assistant.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'utf8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;inputVariables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'line'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'question'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PromptStack&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nc"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ShiftAssistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'shift-assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;defaultVariant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Every PromptVersion property forces replacement, so a hash of the variant in&lt;/span&gt;
    &lt;span class="c1"&gt;// the description cuts a new numbered version whenever anything in it changes.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hex'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnPromptVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ShiftAssistantVersion'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;promptArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`release ${hash.slice(0, 12)}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyRemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RETAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// old versions stay callable&lt;/span&gt;

    &lt;span class="c1"&gt;// The eval stage and the app read the ARN from here: one place to promote or roll back.&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CandidateArn'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;parameterName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'/harbor-works/shift-assistant/candidate-prompt-arn'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;stringValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PromptStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HarborWorksPrompts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'eu-west-1'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rolling back is a &lt;code&gt;git revert&lt;/code&gt;, which cuts a new version with the old content. Not on Bedrock? A &lt;code&gt;prompts/&lt;/code&gt; folder plus a versioned model ID in config does the same job.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pitfall 4: Shipping LLM changes with no eval suite
&lt;/h4&gt;

&lt;p&gt;You'll notice it when prompt changes are "tested" in a playground with three questions, and users find the four answers that broke.&lt;/p&gt;

&lt;p&gt;It happens because LLM output isn't deterministic, so teams decide it can't be tested. It can. You test properties and judgments instead of exact strings, in three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A golden set&lt;/strong&gt; of real questions with reference answers, owned by someone who knows the domain. I'd start with 50 from production logs and add every reported bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap checks first.&lt;/strong&gt; Must include, must not include, valid JSON. They cost nothing and catch a lot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A calibrated judge&lt;/strong&gt; for the rest. In Zheng et al.'s 2023 study, strong LLM judges agreed with humans over 80% of the time, as often as humans agree with each other, but showed position, verbosity and self-enhancement bias. Use a judge from a different model family at temperature 0, and check it against human labels first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On AWS, Bedrock evaluations run judge jobs with built-in metrics such as &lt;code&gt;Builtin.Correctness&lt;/code&gt;, even on responses from other providers. AgentCore Evaluations (GA March 2026) adds on-demand agent regression tests for CI/CD. The open-source choice is promptfoo, still MIT-licensed after joining OpenAI in 2026.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pitfall 5: Treating model end-of-life as a surprise
&lt;/h4&gt;

&lt;p&gt;You'll notice it when a model ID that worked for a year starts returning &lt;code&gt;ResourceNotFoundException: This model version has reached the end of its life.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It happens because nobody put model lifecycles on a calendar. Bedrock models go from Active to Legacy to EOL. AWS publishes the EOL date when a model enters Legacy, usually six months ahead and sometimes only 45 days. In Legacy, existing customers "may lose access after 15 days of inactivity". After EOL, requests fail and "migration will not happen automatically" (&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html" rel="noopener noreferrer"&gt;Bedrock model lifecycle&lt;/a&gt;). Claude Sonnet 4.5 is already Legacy, with EOL on April 8, 2027.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A["Active&amp;lt;br/&amp;gt;earliest EOL on the model card"] --&amp;gt;|"AWS publishes the EOL date"| L["Legacy&amp;lt;br/&amp;gt;6 months or 45 days of notice"]
  L --&amp;gt; E["End of life&amp;lt;br/&amp;gt;requests fail"]
  A -.-&amp;gt; Y1["You: pin the ID&amp;lt;br/&amp;gt;and record the dates"]
  L -.-&amp;gt; Y2["You: eval the successor&amp;lt;br/&amp;gt;and migrate"]
  E -.-&amp;gt; Y3["You: already moved"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The EOL date only appears once a model enters Legacy, so that's when your migration clock starts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is an inventory with an alarm. This check fails a daily build 90 days before trouble.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GetFoundationModel.html" rel="noopener noreferrer"&gt;GetFoundationModel API&lt;/a&gt; and the model lifecycle guide on Oct 29, 2026. Unit-tested with botocore Stubber on boto3 1.43.121. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="s2"&gt;"""Fail (exit 1) if a model we depend on is Legacy or within 90 days of EOL."""&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="c1"&gt;# Base model IDs: strip the eu./us./global. inference-profile prefix.&lt;/span&gt;
&lt;span class="no"&gt;IN_USE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"anthropic.claude-sonnet-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"amazon.nova-2-lite-v1:0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;WARN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bedrock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;at_risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;IN_USE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;life&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_foundation_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"modelDetails"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"modelLifecycle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;eol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;life&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"endOfLifeTime"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# set once the model enters Legacy&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{model_id}: {life.get('status')} eol={eol}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;life&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"LEGACY"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eol&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;eol&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;WARN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;at_risk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;at_risk&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the check from a Region that lists every model you use. In Europe, Sonnet 4.6 runs In-Region only in London (eu-west-2) and Ireland reaches it through the EU profile, so if the call fails in eu-west-1, point the client at eu-west-2.&lt;/p&gt;

&lt;p&gt;Your upgrade runbook is then the eval suite: point a branch at the new model, run the golden set, compare. Runtimes need the same calendar. Lambda deprecates Python 3.10 on October 31, 2026, and Amazon Inspector exports SBOMs in CycloneDX 1.4 or SPDX 2.3 for EC2, ECR and Lambda, so the inventory isn't a spreadsheet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pitfall 6: Tracing a non-deterministic request with a retiring SDK
&lt;/h4&gt;

&lt;p&gt;You'll notice it when a user says the assistant "went in circles". Your logs show a 200 after 14 seconds, but not which prompt version ran or which tools it called.&lt;/p&gt;

&lt;p&gt;It happens for two reasons. One question becomes a chain of model and tool calls that request logs can't show. And many teams use the X-Ray SDKs, which entered maintenance mode on February 25, 2026 and reach end of support on February 25, 2027 (&lt;a href="https://aws.amazon.com/blogs/mt/announcing-aws-x-ray-sdks-daemon-end-of-support-and-opentelemetry-migration" rel="noopener noreferrer"&gt;AWS, 2025&lt;/a&gt;). The X-Ray service itself stays.&lt;/p&gt;

&lt;p&gt;The fix is OpenTelemetry, through AWS Distro for OpenTelemetry (ADOT) or the upstream SDKs, exporting to CloudWatch's OTLP endpoint at &lt;code&gt;https://xray.&amp;lt;region&amp;gt;.amazonaws.com/v1/traces&lt;/code&gt; with SigV4. Follow the GenAI semantic conventions: &lt;code&gt;gen_ai.operation.name&lt;/code&gt; and &lt;code&gt;gen_ai.provider.name&lt;/code&gt; on every inference span, plus &lt;code&gt;gen_ai.request.model&lt;/code&gt; and the &lt;code&gt;gen_ai.usage.*&lt;/code&gt; token counts. Then add one attribute of your own, the prompt version ARN. It turns "answers got worse" into a query.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant U as Operator
  participant API as Assistant API
  participant BR as Bedrock
  participant MCP as Fault history MCP server
  participant CW as CloudWatch OTLP
  U-&amp;gt;&amp;gt;API: Why did line 2 stop
  Note over API: invoke_agent span with the prompt version ARN
  API-&amp;gt;&amp;gt;BR: chat span, gen_ai.request.model
  BR--&amp;gt;&amp;gt;API: tool call requested, token counts
  API-&amp;gt;&amp;gt;MCP: execute_tool span, get_fault_history
  MCP--&amp;gt;&amp;gt;API: last 10 faults
  API-&amp;gt;&amp;gt;BR: second chat span with the tool result
  BR--&amp;gt;&amp;gt;API: answer and finish reason
  API--&amp;gt;&amp;gt;U: answer
  API-&amp;gt;&amp;gt;CW: export all spans over OTLP&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;One question becomes four spans, and the prompt version attribute links a bad answer to the change that caused it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The conventions are young. Every &lt;code&gt;gen_ai.*&lt;/code&gt; attribute is at Development stability, so pin your instrumentation library. Prompt and completion text sits in an opt-in event because it often holds personal data, so leave it off in production by default.&lt;/p&gt;

&lt;p&gt;CloudWatch generative AI observability (GA October 2025) gives you dashboards for model invocations and AgentCore agents, and CloudWatch Omni (GA September 2026) adds traces and evaluators in the IDE. Prefer to own the backend? Jaeger takes the same OTLP spans.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pitfall 7: MCP servers nobody reviewed
&lt;/h4&gt;

&lt;p&gt;You'll notice it when an agent behaves differently and no code changed. Or worse, it behaves the same and quietly does something extra.&lt;/p&gt;

&lt;p&gt;It happens because MCP servers skip the controls you put on libraries. They get installed with an unpinned &lt;code&gt;npx&lt;/code&gt; command, they hold credentials, and their tool descriptions go straight into the model's context. The MCP specification itself says tool annotations "should be considered untrusted, unless obtained from a trusted server."&lt;/p&gt;

&lt;p&gt;That's already been exploited. In September 2025, the unofficial &lt;code&gt;postmark-mcp&lt;/code&gt; npm package shipped 15 clean versions, then version 1.0.16 quietly BCC'd every email it sent to an attacker (&lt;a href="https://www.theregister.com/2025/09/29/postmark_mcp_server_code_hijacked/" rel="noopener noreferrer"&gt;The Register, 2025&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  APP["Shift assistant"] --&amp;gt; PV["Prompt version&amp;lt;br/&amp;gt;Git and Bedrock"]
  PV --&amp;gt; M["Model ID&amp;lt;br/&amp;gt;pinned, has an EOL date"]
  APP --&amp;gt; FW["Agent framework&amp;lt;br/&amp;gt;pinned library"]
  APP --&amp;gt; MCP1["Fault history MCP server&amp;lt;br/&amp;gt;internal, reviewed"]
  APP --&amp;gt; MCP2["Email MCP server&amp;lt;br/&amp;gt;third-party npm package"]
  MCP2 --&amp;gt; CRED["Mail API credentials"]
  MCP2 -.-&amp;gt;|"tool descriptions enter the prompt"| M
  RT["Lambda runtime&amp;lt;br/&amp;gt;deprecation date"] --&amp;gt; APP&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Each box changes on its own schedule, and the third-party server is the one holding credentials, so each needs a pinned version and an owner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is to treat each MCP server as a third-party package with network access. Pin exact versions, never &lt;code&gt;@latest&lt;/code&gt;. Put servers in your SBOM, and give each one least-privilege credentials of its own. Commit a snapshot of the tool schemas with a test that fails when the live tools differ, so a changed description becomes a red build.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Pitfall 8: AI-written code outpacing review
&lt;/h4&gt;

&lt;p&gt;You'll notice it when pull requests get bigger, review turns into skimming, and change fail rate creeps up while everyone feels faster.&lt;/p&gt;

&lt;p&gt;It happens because generating code got cheap and reviewing it didn't. DORA's 2025 report calls AI an amplifier, and its AI Capabilities Model singles out two habits: working in small batches, and strong version control, where frequent commits and frequent rollbacks amplified the gains.&lt;/p&gt;

&lt;p&gt;The fix is to let machines carry the checks so people review intent. Cap pull request size, require tests with generated changes, and enforce lint, types and coverage in CodeBuild. Keep code owners, so generated code still has a human one. If change fail rate rises while lead time falls, you're borrowing against the future.&lt;/p&gt;

&lt;p&gt;The same rules make coding agents useful for upgrades. Kiro (GA November 2025) does spec-driven development with property-based tests. AWS Transform custom (GA December 2025) runs Java 8 to 17, Python and Node.js upgrades from one CLI command. Still on Amazon Q Developer in the IDE? Its plugins reach end of support on April 30, 2027, with Kiro as the named successor.&lt;/p&gt;

&lt;p&gt;Whatever the agent, the gate is the same: it opens a pull request, the full test and eval suite runs, and a person approves. An agent migration without tests is just a very large unreviewed diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Still immature:&lt;/strong&gt; judges need recalibrating, the GenAI conventions are unstable, and MCP has no common standard for signing servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on: an eval gate for prompt and model changes
&lt;/h2&gt;

&lt;p&gt;At Harbor Works, the shift assistant answers operators' questions about line faults. A wrong answer could mean someone clears a jam without locking out the conveyor, so prompt changes ship like code.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  PR["Pull request&amp;lt;br/&amp;gt;prompt, model ID or code"] --&amp;gt; SRC["CodePipeline&amp;lt;br/&amp;gt;source and trigger"]
  SRC --&amp;gt; STG["Deploy to staging&amp;lt;br/&amp;gt;new prompt version"]
  STG --&amp;gt; CB["CodeBuild&amp;lt;br/&amp;gt;lint, unit tests, evals"]
  CB --&amp;gt;|"pass rate at or above the gate"| PROD["Deploy to production&amp;lt;br/&amp;gt;CodeDeploy canary"]
  CB --&amp;gt;|"below the gate or any error"| STOP["Pipeline stops&amp;lt;br/&amp;gt;report lists failing cases"]
  PROD --&amp;gt;|"canary alarm fires"| RB["Automatic rollback&amp;lt;br/&amp;gt;last good revision"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;A prompt or model change can't reach production without passing the same eval gate and canary as code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The staging deploy runs the pitfall 3 stack, which writes the new prompt version ARN to Parameter Store. CodeBuild reads it and runs lint, tests and evals.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html" rel="noopener noreferrer"&gt;CodeBuild buildspec reference&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html" rel="noopener noreferrer"&gt;available runtimes&lt;/a&gt; on Oct 29, 2026. YAML-parsed, not run in CodeBuild — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;JUDGE_MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"eu.amazon.nova-2-lite-v1:0"&lt;/span&gt; &lt;span class="c1"&gt;# a different family from the generator
&lt;/span&gt;    &lt;span class="na"&gt;EVAL_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0.9"&lt;/span&gt;
  &lt;span class="na"&gt;parameter-store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Written by the staging deploy: ARN of the prompt version under test
&lt;/span&gt;    &lt;span class="na"&gt;PROMPT_VERSION_ARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/harbor-works/shift-assistant/candidate-prompt-arn"&lt;/span&gt;

&lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runtime-versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.12&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements-dev.txt&lt;/span&gt; &lt;span class="c1"&gt;# pinned boto3, pytest, ruff
&lt;/span&gt;  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# One phase on purpose: a failing command skips the rest of the phase,
&lt;/span&gt;      &lt;span class="c1"&gt;# so the paid eval step only runs after lint and unit tests pass.
&lt;/span&gt;      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ruff check .&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ruff format --check .&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pytest -q --junitxml=reports/unit.xml&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python evals/run_evals.py evals/golden.jsonl reports/evals.xml&lt;/span&gt;

&lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unit-tests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"unit.xml"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;base-directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports&lt;/span&gt;
    &lt;span class="na"&gt;file-format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JUNITXML&lt;/span&gt;
  &lt;span class="na"&gt;llm-evals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"evals.xml"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;base-directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports&lt;/span&gt;
    &lt;span class="na"&gt;file-format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JUNITXML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line of the golden set is one case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"e17-conveyor-jam"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"vars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Conveyor stopped with fault E-17. What do I do first?"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"E-17 is the jam sensor. Lock out the conveyor before clearing the jam, then reset from the HMI."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"must_include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lock"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"must_not_include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bypass the sensor"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner applies the string checks, asks the judge only when they pass, and writes JUnit XML for CodeBuild's test reports.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the Converse API reference on Oct 29, 2026. Unit-tested with botocore Stubber on boto3 1.43.121 and Python 3.13, ruff clean. Not run against Bedrock — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="s2"&gt;"""Eval gate: run the golden set against a prompt version, write JUnit XML."""&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="no"&gt;ET&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;

&lt;span class="n"&gt;brt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-runtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;PROMPT_ARN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PROMPT_VERSION_ARN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ...:prompt/ID:VERSION&lt;/span&gt;
&lt;span class="no"&gt;JUDGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"JUDGE_MODEL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"EVAL_THRESHOLD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"0.9"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;RUBRIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"Grade a plant-floor assistant.\nQuestion: {q}\nReference: {ref}\n"&lt;/span&gt;
    &lt;span class="s2"&gt;"Candidate: {ans}\nScore 1 only if the candidate agrees with the reference "&lt;/span&gt;
    &lt;span class="s1"&gt;'and gives a safe next step. Reply with JSON only: {{"score": 0 or 1, "reason": "..."}}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# The prompt version pins model, system prompt and inference config, so&lt;/span&gt;
    &lt;span class="c1"&gt;# Converse rejects system/inferenceConfig/toolConfig here. Variables only.&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;PROMPT_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;promptVariables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;text_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"vars"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;JUDGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# a different model family from the generator&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inferenceConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;text_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"\{.*\}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AttributeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"unparseable judge output: {raw[:120]}"&lt;/span&gt;  &lt;span class="c1"&gt;# fail closed&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""None if the case passes, otherwise the reason it failed."""&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"vars"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"must_include"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;banned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"must_not_include"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# cheap string checks before paying for a judge call&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"missing={missing} banned={banned}"&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"judge: {reason}"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;junit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;suite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"testsuite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"llm-evals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# a Bedrock error raises and fails the build: fail closed&lt;/span&gt;
        &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SubElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"testcase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;classname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"golden"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="no"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SubElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"failures"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ElementTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;junit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml_declaration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"pass rate {rate:.1%} on {len(cases)} cases (gate {THRESHOLD:.0%})"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="no"&gt;THRESHOLD&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you run it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permissions.&lt;/strong&gt; The build role needs &lt;code&gt;ssm:GetParameters&lt;/code&gt;, &lt;code&gt;bedrock:RenderPrompt&lt;/code&gt;, &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; on the inference profiles and the foundation models in every Region they route to, and the CodeBuild report actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold.&lt;/strong&gt; Run the suite on today's prompt, set the gate just below that score, and raise it as the golden set grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Each passing case costs two model calls, plus build minutes ($0.005 a minute for &lt;code&gt;general1.small&lt;/code&gt; in AWS's pricing example).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the pitfall 5 lifecycle check as another command, and a model EOL date fails the build three months early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: which tools, and what it costs elsewhere
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choose the first when&lt;/th&gt;
&lt;th&gt;Choose the second when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CDK Pipelines vs. GitHub Actions&lt;/td&gt;
&lt;td&gt;Your infrastructure is CDK and you want a self-updating pipeline&lt;/td&gt;
&lt;td&gt;You deploy Terraform or mixed tools, or already live in GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDK vs. Terraform&lt;/td&gt;
&lt;td&gt;AWS-only, TypeScript team, you want &lt;code&gt;cdk drift&lt;/code&gt; and drift-aware change sets&lt;/td&gt;
&lt;td&gt;Multi-cloud or SaaS providers, or &lt;code&gt;plan&lt;/code&gt; review is your change control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock Prompt Management vs. prompt files only&lt;/td&gt;
&lt;td&gt;You're on Bedrock and want versions callers can't override&lt;/td&gt;
&lt;td&gt;You use several model providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock or AgentCore evaluations vs. promptfoo&lt;/td&gt;
&lt;td&gt;You want managed jobs, built-in metrics or agent checks&lt;/td&gt;
&lt;td&gt;You want evals beside unit tests, for any provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch GenAI observability vs. open-source OTLP backend&lt;/td&gt;
&lt;td&gt;Mostly AWS, and you want AgentCore dashboards with no extra infrastructure&lt;/td&gt;
&lt;td&gt;Multi-cloud, or you must own retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Maintainability work pulls on the other eight requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Other NFR&lt;/th&gt;
&lt;th&gt;What maintainability work does to it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Pipelines concentrate power, so guard the pipeline role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;Git history and prompt versions record who changed what&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Checks such as axe-core run in the same build as lint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Canaries add minutes per deploy, tracing a little per request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Small, reversible changes lower change fail rate, and drift breaks failovers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;IaC stamps out new cells and Regions instead of hand-building them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Evals spend tokens on every change, and traces cost storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustainability&lt;/td&gt;
&lt;td&gt;Eval runs and long trace retention burn hidden compute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who: owners and the shared responsibility split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Product owner&lt;/th&gt;
&lt;th&gt;Tech lead&lt;/th&gt;
&lt;th&gt;Developers&lt;/th&gt;
&lt;th&gt;Platform / SRE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline, canaries, rollback alarms&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC and drift checks&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden set and eval threshold&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt and model versions&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model, runtime and MCP inventory&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review rules for AI-generated code&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;R = responsible, A = accountable, C = consulted, I = informed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Who patches what depends on the compute type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2.&lt;/strong&gt; You patch the OS, agents and AMIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers.&lt;/strong&gt; You own images and base-image updates. On Fargate, AWS patches the host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda.&lt;/strong&gt; AWS patches the runtime. You move off it before its block dates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed AI (Bedrock).&lt;/strong&gt; AWS and the provider run the model and publish its lifecycle dates. You pin IDs, own the evals and migrate before EOL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Make the pipeline the only way to production, with alarm-based rollback on one service.&lt;/li&gt;
&lt;li&gt;[ ] Run a drift check against production and fix what it finds in code.&lt;/li&gt;
&lt;li&gt;[ ] Move every prompt and model ID into Git, then search for model ID strings anywhere else.&lt;/li&gt;
&lt;li&gt;[ ] Write 20 golden cases from real questions and record today's pass rate.&lt;/li&gt;
&lt;li&gt;[ ] Check your judge against 50 human-labelled answers before you gate on it.&lt;/li&gt;
&lt;li&gt;[ ] Add the model lifecycle check as a daily build.&lt;/li&gt;
&lt;li&gt;[ ] Put the prompt version ARN on your spans, and plan the move off the X-Ray SDKs before February 25, 2027.&lt;/li&gt;
&lt;li&gt;[ ] Pin every MCP server to an exact version and commit its tool schemas.&lt;/li&gt;
&lt;li&gt;[ ] Track change fail rate and deployment rework rate per service.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Knight Capital didn't lose $460 million to a hard problem. It lost it to a manual deploy, a dead feature and a flag with two meanings. Every pitfall here is a system nobody can fully see or safely change.&lt;/p&gt;

&lt;p&gt;AI gives the old mistakes new places to hide. A prompt edited in a console is a manual deploy. A model past its EOL date is an unpatched runtime. An MCP server on &lt;code&gt;@latest&lt;/code&gt; is an unpinned dependency holding credentials. The fixes are the ones you know: version it, test it, trace it, and keep a way back. The new part is the eval suite, because "does it still work?" now has a fuzzy answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give every change one meaning, one owner and one way back.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Availability&lt;/li&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Cost (coming soon)&lt;/li&gt;
&lt;li&gt;Sustainability (coming soon)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up, Cost:&lt;/strong&gt; one real, sourced case study of a team cutting its AWS bill, and what per-token pricing does to unit economics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sec.gov/litigation/admin/2013/34-70694.pdf" rel="noopener noreferrer"&gt;In the Matter of Knight Capital Americas LLC, Administrative Proceeding File No. 3-15570 — U.S. SEC, October 2013&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sec.gov/newsroom/press-releases/2013-222" rel="noopener noreferrer"&gt;SEC Charges Knight Capital With Violations of Market Access Rule — U.S. SEC press release, October 2013&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.crowdstrike.com/wp-content/uploads/2024/08/Channel-File-291-Incident-Root-Cause-Analysis-08.06.2024.pdf" rel="noopener noreferrer"&gt;Channel File 291 Incident Root Cause Analysis — CrowdStrike, August 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://quality.arc42.org/qualities/maintainability" rel="noopener noreferrer"&gt;Maintainability (ISO/IEC 25010:2023 definition) — arc42 quality model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;DORA's software delivery performance metrics — DORA, updated January 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dora.dev/guides/dora-metrics/history/" rel="noopener noreferrer"&gt;A history of DORA's software delivery metrics — DORA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2203.04374" rel="noopener noreferrer"&gt;Code Red: The Business Impact of Code Quality — Adam Tornhill and Markus Borg, arXiv, 2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/products/devops-sre/announcing-the-2024-dora-report" rel="noopener noreferrer"&gt;Announcing the 2024 DORA report — Google Cloud, October 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report" rel="noopener noreferrer"&gt;Announcing the 2025 DORA report — Google Cloud, September 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://research.google/pubs/dora-2025-state-of-ai-assisted-software-development-report/" rel="noopener noreferrer"&gt;DORA 2025 State of AI-assisted Software Development Report — Google Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/introducing-doras-inaugural-ai-capabilities-model" rel="noopener noreferrer"&gt;Introducing DORA's inaugural AI Capabilities Model — Google Cloud, September 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dora.dev/insights/dora-2025-year-in-review/" rel="noopener noreferrer"&gt;DORA 2025: Year in review — DORA, January 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dora.dev/research/" rel="noopener noreferrer"&gt;DORA research by year — DORA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity — METR, July 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://metr.org/blog/2026-02-24-uplift-update/" rel="noopener noreferrer"&gt;Uplift update — METR, February 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/deployment-configurations.html" rel="noopener noreferrer"&gt;Working with deployment configurations in CodeDeploy — CodeDeploy docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/deployments-rollback-and-redeploy.html" rel="noopener noreferrer"&gt;Redeploy and roll back a deployment with CodeDeploy — CodeDeploy docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/cdk-pipeline.html" rel="noopener noreferrer"&gt;Continuous integration and delivery with CDK Pipelines — AWS CDK v2 guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/devops/aws-codecommit-returns-to-general-availability" rel="noopener noreferrer"&gt;AWS CodeCommit returns to general availability — AWS DevOps Blog, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-drift.html" rel="noopener noreferrer"&gt;cdk drift — AWS CDK v2 guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html" rel="noopener noreferrer"&gt;Detect unmanaged configuration changes with drift detection — CloudFormation docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/drift-aware-change-sets.html" rel="noopener noreferrer"&gt;Using drift-aware change sets — CloudFormation docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/configuration-drift-enhanced-cloudformation-sets" rel="noopener noreferrer"&gt;Safely handle configuration drift with CloudFormation drift-aware change sets — AWS, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/config/latest/developerguide/cloudformation-stack-drift-detection-check.html" rel="noopener noreferrer"&gt;cloudformation-stack-drift-detection-check — AWS Config docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/commands/plan" rel="noopener noreferrer"&gt;terraform plan command — HashiCorp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html" rel="noopener noreferrer"&gt;Prompt management in Amazon Bedrock — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management-version-create.html" rel="noopener noreferrer"&gt;Create a version of a prompt in Prompt management — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Converse — Amazon Bedrock API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-bedrock-promptversion.html" rel="noopener noreferrer"&gt;AWS::Bedrock::PromptVersion — CloudFormation template reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrock.CfnPrompt.html" rel="noopener noreferrer"&gt;CfnPrompt — AWS CDK API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2306.05685" rel="noopener noreferrer"&gt;Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — Zheng et al., arXiv, 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation-judge.html" rel="noopener noreferrer"&gt;Evaluate model performance using another LLM as a judge — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation-metrics.html" rel="noopener noreferrer"&gt;Use metrics to understand model performance — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/pricing/" rel="noopener noreferrer"&gt;Amazon Bedrock pricing (model evaluation) — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/agentcore-evaluations-generally-available" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Evaluations is now generally available — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/promptfoo/promptfoo" rel="noopener noreferrer"&gt;promptfoo — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.techzine.eu/news/devops/139411/openai-announces-acquisition-of-ai-testing-startup-promptfoo" rel="noopener noreferrer"&gt;OpenAI announces acquisition of AI testing startup Promptfoo — Techzine, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html" rel="noopener noreferrer"&gt;Model lifecycle — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-4-5.html" rel="noopener noreferrer"&gt;Claude Sonnet 4.5 model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-4-6.html" rel="noopener noreferrer"&gt;Claude Sonnet 4.6 model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html" rel="noopener noreferrer"&gt;Amazon Nova 2 Lite model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GetFoundationModel.html" rel="noopener noreferrer"&gt;GetFoundationModel — Amazon Bedrock API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html" rel="noopener noreferrer"&gt;Lambda runtimes and deprecation policy — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/inspector/latest/user/sbom-export.html" rel="noopener noreferrer"&gt;Exporting SBOMs with Amazon Inspector — Inspector docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/mt/announcing-aws-x-ray-sdks-daemon-end-of-support-and-opentelemetry-migration" rel="noopener noreferrer"&gt;Announcing AWS X-Ray SDKs/Daemon end of support and OpenTelemetry migration — AWS Cloud Operations Blog, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-daemon-timeline.html" rel="noopener noreferrer"&gt;X-Ray SDK and daemon support timeline — X-Ray docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html" rel="noopener noreferrer"&gt;OTLP endpoints — CloudWatch docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions repository — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dash0.com/knowledge/opentelemetry-genai-semantic-conventions-explained" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions explained — Dash0, September 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/10/generative-ai-observability-amazon-cloudwatch" rel="noopener noreferrer"&gt;Generative AI observability now generally available for Amazon CloudWatch — AWS, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/GenAI-observability.html" rel="noopener noreferrer"&gt;Generative AI observability — CloudWatch docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/introducing-amazon-cloudwatch-omni-ai-powered-observability-for-generative-ai-and-agentic-workloads/" rel="noopener noreferrer"&gt;Introducing Amazon CloudWatch Omni — AWS News Blog, September 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/latest" rel="noopener noreferrer"&gt;Model Context Protocol specification (2026-07-28) — modelcontextprotocol.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/2025/09/29/postmark_mcp_server_code_hijacked/" rel="noopener noreferrer"&gt;Postmark MCP server code hijacked — The Register, September 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://siliconangle.com/?p=721187" rel="noopener noreferrer"&gt;Kiro reaches general availability — SiliconANGLE, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/blog/one-year/" rel="noopener noreferrer"&gt;One year of Kiro — Kiro blog, July 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/transform-custom-organization-wide-modernization" rel="noopener noreferrer"&gt;AWS launches AWS Transform custom — AWS, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/devops/amazon-q-developer-end-of-support-announcement/" rel="noopener noreferrer"&gt;Amazon Q Developer end-of-support announcement — AWS DevOps Blog, April 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-developer-ide-end-of-support.html" rel="noopener noreferrer"&gt;Amazon Q Developer IDE plugins end of support — Amazon Q Developer docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html" rel="noopener noreferrer"&gt;Buildspec reference — CodeBuild docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html" rel="noopener noreferrer"&gt;Docker images provided by CodeBuild (available runtimes) — CodeBuild docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/test-permissions.html" rel="noopener noreferrer"&gt;Test report permissions — CodeBuild docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html" rel="noopener noreferrer"&gt;Prerequisites for inference profiles — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/codebuild/pricing/" rel="noopener noreferrer"&gt;AWS CodeBuild pricing — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/10/aws-service-availability" rel="noopener noreferrer"&gt;AWS service availability updates — AWS, October 2025&lt;/a&gt;, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability" rel="noopener noreferrer"&gt;March 2026&lt;/a&gt;, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/aws-service-availability/" rel="noopener noreferrer"&gt;June 2026&lt;/a&gt;, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-service-availability/" rel="noopener noreferrer"&gt;September 2026&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Scalability</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 11 Nov 2026 15:02:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-scalability-24i</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-scalability-24i</guid>
      <description>&lt;p&gt;In 2023, the Prime Video team that monitors stream quality published something few teams admit to. Their first design, a distributed pipeline of serverless components orchestrated by AWS Step Functions, was "proven to only support around 5% of the expected load." It hit an account limit on Step Functions state transitions and paid for heavy S3 traffic on intermediate video frames. They moved the logic into one process on ECS on EC2 and reported a 90% cut in operating costs (&lt;a href="https://www.infoq.com/news/2023/05/prime-ec2-ecs-saves-costs/" rel="noopener noreferrer"&gt;InfoQ, 2023&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Nobody on that team picked bad services. Each one scaled fine on its own. What broke was the cost per unit of work, which grew faster than the load. Every scaling option buys you something and charges you for it, so let's put numbers on both.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalability means cost per unit of work stays flat as load grows. Elasticity is how fast capacity follows it.&lt;/li&gt;
&lt;li&gt;Price one unit of load per option. In us-east-1, Lambda beats always-on containers only while they'd sit below about 35–40% busy.&lt;/li&gt;
&lt;li&gt;Your database is usually the real ceiling. Find its limit before adding compute in front of it.&lt;/li&gt;
&lt;li&gt;For AI, the scaling unit is &lt;strong&gt;tokens per minute&lt;/strong&gt; and your Bedrock quota is the ceiling. Plan quota like capacity.&lt;/li&gt;
&lt;li&gt;A queue with a concurrency cap is often cheaper than more capacity.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What: scalability, elasticity and performance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt; is the ability to take on more work by adding resources while the cost of each unit of work stays roughly flat. Twice the users for twice the money scales. A wall you can't spend past doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Elasticity&lt;/strong&gt; is how quickly and automatically capacity follows the load. &lt;strong&gt;Performance&lt;/strong&gt;, from Part 5, is how fast one request is at a given load. A fast system can fail to scale, and an elastic one can scale into a bill nobody planned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7rj4v8lcggeg35q76l99.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7rj4v8lcggeg35q76l99.png" alt="Three questions people blur together. Performance: how fast is one request at a given load? Elasticity: how quickly and automatically does capacity follow the load? Scalability: does the cost of each unit of work stay roughly flat as the load grows? A fast system can fail to scale. An elastic one can scale into a bill nobody planned." width="799" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertical scaling&lt;/strong&gt; means a bigger machine. It needs no code changes and stops at the largest instance size, with a restart on the way up. &lt;strong&gt;Horizontal scaling&lt;/strong&gt; means more machines behind a load balancer. It has no hard top, but only when any copy can serve any request, so your app has to be &lt;strong&gt;stateless&lt;/strong&gt;, with sessions and caches in a shared store.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph V["Vertical: a bigger box"]
    U1["Users"] --&amp;gt; B1["One large instance&amp;lt;br/&amp;gt;app plus session state"]
    B1 --&amp;gt; C1["Ceiling: largest size,&amp;lt;br/&amp;gt;restart to resize"]
  end
  subgraph H["Horizontal: more boxes"]
    U2["Users"] --&amp;gt; LB["Load balancer"]
    LB --&amp;gt; I1["Stateless copy 1"]
    LB --&amp;gt; I2["Stateless copy 2"]
    LB --&amp;gt; I3["Stateless copy N"]
    I1 --&amp;gt; S["Shared state&amp;lt;br/&amp;gt;DynamoDB or ElastiCache"]
    I2 --&amp;gt; S
    I3 --&amp;gt; S
    S --&amp;gt; C2["Ceiling moves to&amp;lt;br/&amp;gt;the shared store"]
  end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Horizontal scaling removes the ceiling from compute and moves it to the shared state store.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To test it, track &lt;strong&gt;cost per unit of work&lt;/strong&gt; (per 1,000 requests or per million tokens), &lt;strong&gt;maximum throughput&lt;/strong&gt; at your latency SLO, &lt;strong&gt;scale-out time&lt;/strong&gt; from a load step to serving capacity, and &lt;strong&gt;headroom&lt;/strong&gt;, the capacity you pay for to cover that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: scaling fails in two expensive directions
&lt;/h2&gt;

&lt;p&gt;Under-scaling costs you the event you were built for. On November 15, 2022, the Eras Tour presale sent Ticketmaster 3.5 billion system requests, four times its previous peak, and about 15% of site interactions had issues (&lt;a href="https://business.ticketmaster.com/press-release/taylor-swift-the-eras-tour-onsale-explained/" rel="noopener noreferrer"&gt;Ticketmaster, 2022&lt;/a&gt;). Ticketmaster blamed bots and fans without codes. Either way, the bill came due on launch day.&lt;/p&gt;

&lt;p&gt;Over-scaling costs you every month, quietly. In Flexera's 2026 State of the Cloud report, respondents estimated that 29% of their cloud spend is wasted, the first rise in five years, with AI named as a growing source of it (&lt;a href="https://www.globenewswire.com/de/news-release/2026/03/18/3258065/0/en/flexera-finds-cloud-value-is-rising-while-ai-waste-grows.html" rel="noopener noreferrer"&gt;Flexera, March 2026&lt;/a&gt;). Headroom you add for a launch and never take back is an easy way to land in that number.&lt;/p&gt;

&lt;p&gt;And the ceiling is rarely AWS itself. During Prime Day 2025, DynamoDB peaked at 151 million requests per second and SQS at 166 million messages per second (AWS News Blog, 2025). Your design decides how far you get, and at what price.&lt;/p&gt;

&lt;h2&gt;
  
  
  When: where scalability enters the lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design.&lt;/strong&gt; Name your scaling unit and the component with the lowest ceiling. Price one unit at today's load and at ten times it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Keep compute stateless, make writes idempotent, and pool database connections from the first commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test.&lt;/strong&gt; Load test past your peak until something breaks, with a sudden step, since a smooth ramp hides warm-up gaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate.&lt;/strong&gt; Put cost per unit next to latency on the dashboard. Alarm on queue age and quota use, not only CPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The database is the real ceiling
&lt;/h2&gt;

&lt;p&gt;Here's the pattern I'd expect in most designs. Compute scales out cleanly, and every new copy opens a connection to the same database. Lambda can add 1,000 execution environments every 10 seconds per function. An RDS for MySQL &lt;code&gt;db.m7g.large&lt;/code&gt; allows about 630 connections by default (&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html" rel="noopener noreferrer"&gt;RDS quotas&lt;/a&gt;). One burst can use up the pool in seconds.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  B["Traffic burst"] --&amp;gt; L["Lambda adds up to 1,000&amp;lt;br/&amp;gt;environments per 10 s"]
  L --&amp;gt; Q{"Where does&amp;lt;br/&amp;gt;each one connect?"}
  Q --&amp;gt;|"Directly"| DB["RDS db.m7g.large&amp;lt;br/&amp;gt;about 630 connections"]
  DB --&amp;gt; X["Too many connections&amp;lt;br/&amp;gt;errors under load"]
  Q --&amp;gt;|"Through RDS Proxy"| P["Shared connection pool"]
  P --&amp;gt; DB2["Same database,&amp;lt;br/&amp;gt;steady connection count"]
  Q --&amp;gt;|"Key-value access"| D["DynamoDB&amp;lt;br/&amp;gt;3,000 RCU and 1,000 WCU&amp;lt;br/&amp;gt;per partition"]
  D --&amp;gt; K{"Partition key&amp;lt;br/&amp;gt;spread evenly?"}
  K --&amp;gt;|"No"| HOT["Hot partition throttles&amp;lt;br/&amp;gt;while the table looks idle"]
  K --&amp;gt;|"Yes, or write sharding"| OK["Throughput grows&amp;lt;br/&amp;gt;with partitions"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Scaling compute only helps when the database behind it can accept the extra connections and writes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your options, from cheapest change to most expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RDS Proxy&lt;/strong&gt; pools connections for $0.015 per database vCPU-hour in us-east-1, so a 2-vCPU instance adds 2 × $0.015 × 730 = $21.90 a month. Statements over 16 KB and some session state pin a client to one connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read replicas&lt;/strong&gt; take reads. Aurora allows up to 15, with lag "usually much less than 100 milliseconds." They do nothing for writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; gives each partition 3,000 read and 1,000 write units per second, so a key like &lt;code&gt;plant#date&lt;/code&gt; that every write hits is your ceiling however big the table gets. &lt;strong&gt;Write sharding&lt;/strong&gt; adds a suffix (say 1–200) to spread writes, at the cost of reading every shard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharding a relational database&lt;/strong&gt; is the last resort, because cross-shard joins become your problem. Aurora PostgreSQL Limitless Database (generally available since October 2024) does the routing, but prove it on your queries first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where: what each option buys you
&lt;/h2&gt;

&lt;p&gt;Each option trades money for work you no longer do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EC2 Auto Scaling&lt;/strong&gt; adds no fee beyond the instances. AWS says to "set the target value as high as possible with a reasonable buffer," because headroom is money. Predictive scaling needs 24 hours of history (14 days is better), forecasts 48 hours ahead and only scales out. Harbor Works, the fictional plant from the Luso companion app, has three production lines whose dashboards fill at every shift change, the textbook predictive case. Choose EC2 for steady load with Savings Plans (up to 72% off), unless you don't want to own AMIs and patching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ECS on Fargate&lt;/strong&gt; is the middle ground, with no hosts, per-second billing and Fargate Spot at up to 70% off for interruptible work. It fits a containerized API with daytime load and a small team, not GPU work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS&lt;/strong&gt; pays off when many workloads share one platform. The HPA scales pods, KEDA scales them from sources like SQS depth, and Karpenter picks and packs nodes. A NodePool's &lt;code&gt;limits&lt;/code&gt; is a spending ceiling, and &lt;code&gt;consolidationPolicy: WhenEmptyOrUnderutilized&lt;/code&gt; repacks pods onto fewer nodes. Choose EKS when Kubernetes is already your platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lambda&lt;/strong&gt; stays the default for spiky and low-traffic work. Its limit is concurrency, not money, which is why its most useful scaling setting is a cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: price one unit of load
&lt;/h2&gt;

&lt;p&gt;Let's compare options on one unit, &lt;strong&gt;one vCPU and 2 GB of memory kept busy for one hour&lt;/strong&gt;, using us-east-1 on-demand prices from the AWS Price List API, pulled October 29, 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Price per busy unit-hour&lt;/th&gt;
&lt;th&gt;How it scales&lt;/th&gt;
&lt;th&gt;AWS handles&lt;/th&gt;
&lt;th&gt;You still own&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lambda, Arm, 2 GB&lt;/td&gt;
&lt;td&gt;$0.096 + $0.0072 for 36,000 requests at 100 ms = &lt;strong&gt;$0.103&lt;/strong&gt;, only while running&lt;/td&gt;
&lt;td&gt;1,000 environments per 10 s, 1,000 per Region by default&lt;/td&gt;
&lt;td&gt;Servers, OS, AZs, scaling&lt;/td&gt;
&lt;td&gt;Concurrency caps, connections, idempotency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Managed Instances&lt;/td&gt;
&lt;td&gt;EC2 price + 15% fee&lt;/td&gt;
&lt;td&gt;Doubles in about 5 min, on CPU&lt;/td&gt;
&lt;td&gt;Instances, patching&lt;/td&gt;
&lt;td&gt;Thread safety, instance choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS on Fargate, Arm&lt;/td&gt;
&lt;td&gt;$0.03238 + 2 × $0.00356 = &lt;strong&gt;$0.0395&lt;/strong&gt; per running hour&lt;/td&gt;
&lt;td&gt;Target tracking&lt;/td&gt;
&lt;td&gt;Hosts, OS&lt;/td&gt;
&lt;td&gt;Task size, policy, AZ placement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 Auto Scaling, c7g.large&lt;/td&gt;
&lt;td&gt;$0.0725 ÷ 2 = &lt;strong&gt;$0.0363&lt;/strong&gt;, or $0.0239 with a 1-year RI&lt;/td&gt;
&lt;td&gt;Target tracking plus predictive&lt;/td&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;OS, AMIs, warm-up, headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EKS with Karpenter&lt;/td&gt;
&lt;td&gt;EC2 price + $73/month per cluster on standard support&lt;/td&gt;
&lt;td&gt;HPA or KEDA, plus Karpenter&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;td&gt;Nodes, add-ons, upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Containers and instances look cheaper, but you pay for them while they're idle. At a 60% CPU target, a busy unit-hour really costs $0.0363 ÷ 0.6 = $0.060 on EC2 and $0.0395 ÷ 0.6 = $0.066 on Fargate. Lambda bills only while it runs.&lt;/p&gt;

&lt;p&gt;That gives you a break-even line. Fargate wins once its capacity would be busy more than $0.0395 ÷ $0.103 ≈ 38% of the time, EC2 above about 35%. Below that, Lambda wins, and at zero traffic it costs nothing.&lt;/p&gt;

&lt;p&gt;Three caveats. A 2 GB function gets slightly more than one vCPU (1,769 MB equals one). Lambda bills wall-clock time, including waits on a database, while a container serves other requests during the wait, so I/O-heavy work tips toward containers sooner. And none of this prices your team's time.&lt;/p&gt;

&lt;p&gt;So here's the rule I'd use. Start on Lambda when traffic is spiky or unknown. Move a service to Fargate or Lambda Managed Instances once your dashboard shows it busy for most of the day. Reach for EKS when the platform already exists for other reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  What more scale costs elsewhere
&lt;/h3&gt;

&lt;p&gt;Every gain in scale gets paid for somewhere else in this series.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NFR&lt;/th&gt;
&lt;th&gt;Helps&lt;/th&gt;
&lt;th&gt;Hurts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Headroom and Priority tier cut queueing&lt;/td&gt;
&lt;td&gt;Queue levelling adds latency by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Stateless copies across AZs survive failures&lt;/td&gt;
&lt;td&gt;Each autoscaled dependency adds a quota that can run out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Smaller, identical units are easier to patch&lt;/td&gt;
&lt;td&gt;Every new node pool, proxy and queue is more attack surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;In-Region and geographic inference profiles keep processing in one geography&lt;/td&gt;
&lt;td&gt;Global profiles move prompts across Regions to find capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Managed scaling means less code to own&lt;/td&gt;
&lt;td&gt;EKS, Karpenter and self-hosted vLLM add upgrade work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Scale-to-zero, Flex and batch cut idle spend&lt;/td&gt;
&lt;td&gt;Headroom, Reserved tiers and GPU nodes bill whether busy or not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustainability&lt;/td&gt;
&lt;td&gt;Consolidation and Arm instances mean fewer idle watts&lt;/td&gt;
&lt;td&gt;Over-provisioned GPUs burn energy for no work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Queue-based load levelling
&lt;/h2&gt;

&lt;p&gt;A queue lets you size for the average instead of the peak. Producers write as fast as they like and consumers work at a rate your downstream systems survive, for $0.40 per million SQS standard requests after the free million.&lt;/p&gt;

&lt;p&gt;On Lambda, the control is the event source mapping's &lt;code&gt;MaximumConcurrency&lt;/code&gt;, from 2 to 1,000. Without it, Lambda adds up to 300 concurrent invocations a minute, up to 1,250 per mapping (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html" rel="noopener noreferrer"&gt;Lambda docs&lt;/a&gt;). That's great for throughput and bad for a database with 630 connections. Provisioned mode goes the other way, reserving pollers at $0.00925 per poller-hour, and can't be combined with the cap.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  L1["Line 1 shift end"] --&amp;gt; Q["SQS queue&amp;lt;br/&amp;gt;absorbs the burst"]
  L2["Line 2 shift end"] --&amp;gt; Q
  L3["Line 3 shift end"] --&amp;gt; Q
  Q --&amp;gt; E["Event source mapping&amp;lt;br/&amp;gt;MaximumConcurrency 3"]
  E --&amp;gt; W["Lambda workers&amp;lt;br/&amp;gt;one model call each"]
  W --&amp;gt; BR["Amazon Bedrock&amp;lt;br/&amp;gt;TPM quota"]
  W --&amp;gt;|"Throttled: hand back"| Q
  Q --&amp;gt;|"Fails 5 times"| DLQ["Dead-letter queue"]
  Q -.-&amp;gt;|"Oldest message age"| A["CloudWatch alarm&amp;lt;br/&amp;gt;backlog over 15 min"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The queue turns a burst from three lines finishing their shifts at once into a steady stream that stays under the quota.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're trading latency for a flat bill, so the queue's age becomes the SLO. Alarm on &lt;code&gt;ApproximateAgeOfOldestMessage&lt;/code&gt;, not queue length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on: size a queue consumer from your quota
&lt;/h2&gt;

&lt;p&gt;At shift end, Harbor Works asks a model to summarize each line's telemetry for the next supervisor, and all three lines finish at once. Let's size the consumers from the Bedrock quota.&lt;/p&gt;

&lt;p&gt;Each call reserves its input tokens plus &lt;code&gt;maxTokens&lt;/code&gt; against your TPM quota when it starts, and each worker makes 60 ÷ &lt;code&gt;seconds_per_call&lt;/code&gt; calls a minute. So the safe worker count is &lt;code&gt;quota × seconds_per_call ÷ (60 × tokens_per_call)&lt;/code&gt;. With an example quota of 200,000 TPM, 3,400 tokens per call and 4-second calls, that's 200,000 × 4 ÷ (60 × 3,400) ≈ 3.9, so three workers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the Terraform AWS provider docs for &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping" rel="noopener noreferrer"&gt;aws_lambda_event_source_mapping&lt;/a&gt; and &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_policy" rel="noopener noreferrer"&gt;aws_autoscaling_policy&lt;/a&gt;, and the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html" rel="noopener noreferrer"&gt;Lambda SQS configuration guide&lt;/a&gt;, on Oct 29, 2026. &lt;code&gt;terraform validate&lt;/code&gt; passes with Terraform 1.13.4 and AWS provider 6.71.0. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"worker_zip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;# zip containing worker.py (below)&lt;/span&gt;
&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"worker_role_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# needs SQS read/delete + bedrock:InvokeModel&lt;/span&gt;

&lt;span class="n"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Size the fleet from the quota, not from the queue depth.&lt;/span&gt;
  &lt;span class="n"&gt;tpm_quota&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200000&lt;/span&gt; &lt;span class="c1"&gt;# example value: read yours from Service Quotas&lt;/span&gt;
  &lt;span class="n"&gt;tokens_per_call&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3400&lt;/span&gt;   &lt;span class="c1"&gt;# input tokens + maxTokens, reserved when a call starts&lt;/span&gt;
  &lt;span class="n"&gt;seconds_per_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;      &lt;span class="c1"&gt;# measured p50 duration of one worker invocation&lt;/span&gt;
  &lt;span class="n"&gt;max_workers&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tpm_quota&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds_per_call&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens_per_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;fn_timeout&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sqs_queue"&lt;/span&gt; &lt;span class="s2"&gt;"dlq"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"harbor-summaries-dlq"&lt;/span&gt;
  &lt;span class="n"&gt;message_retention_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1209600&lt;/span&gt; &lt;span class="c1"&gt;# 14 days to inspect bad messages&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sqs_queue"&lt;/span&gt; &lt;span class="s2"&gt;"jobs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"harbor-summaries"&lt;/span&gt;
  &lt;span class="n"&gt;visibility_timeout_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_timeout&lt;/span&gt; &lt;span class="c1"&gt;# AWS guidance: at least 6x the function timeout&lt;/span&gt;
  &lt;span class="n"&gt;redrive_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;deadLetterTargetArn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;
    &lt;span class="n"&gt;maxReceiveCount&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="c1"&gt;# throttled jobs come back, broken ones stop here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"worker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;function_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"harbor-summary-worker"&lt;/span&gt;
  &lt;span class="n"&gt;role&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_role_arn&lt;/span&gt;
  &lt;span class="n"&gt;filename&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_zip&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"worker.handler"&lt;/span&gt;
  &lt;span class="n"&gt;runtime&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python3.13"&lt;/span&gt;
  &lt;span class="n"&gt;architectures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arm64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;memory_size&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;
  &lt;span class="n"&gt;timeout&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_timeout&lt;/span&gt;
  &lt;span class="c1"&gt;# Must be &amp;gt;= the event source's maximum concurrency, or Lambda throttles messages.&lt;/span&gt;
  &lt;span class="n"&gt;reserved_concurrent_executions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="no"&gt;MODEL_ID&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"global.amazon.nova-2-lite-v1:0"&lt;/span&gt;
      &lt;span class="no"&gt;SERVICE_TIER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"flex"&lt;/span&gt; &lt;span class="c1"&gt;# async work: 50% off Standard, slower is fine&lt;/span&gt;
      &lt;span class="no"&gt;MAX_TOKENS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"400"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_event_source_mapping"&lt;/span&gt; &lt;span class="s2"&gt;"jobs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;event_source_arn&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;
  &lt;span class="n"&gt;function_name&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;
  &lt;span class="n"&gt;batch_size&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# one model call per invoke keeps the sizing math honest&lt;/span&gt;
  &lt;span class="n"&gt;function_response_types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ReportBatchItemFailures"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;scaling_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;maximum_concurrency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt; &lt;span class="c1"&gt;# the ceiling that protects the TPM quota&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# The queue's age is the SLO you are trading for a flat model bill.&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_metric_alarm"&lt;/span&gt; &lt;span class="s2"&gt;"backlog_age"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;alarm_name&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"harbor-summaries-too-old"&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS/SQS"&lt;/span&gt;
  &lt;span class="n"&gt;metric_name&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ApproximateAgeOfOldestMessage"&lt;/span&gt;
  &lt;span class="n"&gt;dimensions&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;QueueName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;statistic&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Maximum"&lt;/span&gt;
  &lt;span class="n"&gt;period&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
  &lt;span class="n"&gt;evaluation_periods&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="n"&gt;threshold&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="c1"&gt;# 15 minutes: time to raise the quota or the tier&lt;/span&gt;
  &lt;span class="n"&gt;comparison_operator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker makes one attempt per message and hands throttled messages back to the queue, so the queue does the retrying instead of the SDK.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Bedrock Converse API&lt;/a&gt; and the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html" rel="noopener noreferrer"&gt;Nova 2 Lite model card&lt;/a&gt; on Oct 29, 2026. Unit-tested with botocore Stubber on boto3 1.43.121 (a success, a throttle handed back, a validation error raised). Package a current boto3 with the function, because &lt;code&gt;serviceTier&lt;/code&gt; needs a recent SDK. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;ClientError&lt;/span&gt;

&lt;span class="c1"&gt;# One attempt only: the queue is the retry mechanism, not the SDK.&lt;/span&gt;
&lt;span class="n"&gt;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-runtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"max_attempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MODEL_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;SERVICE_TIER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SERVICE_TIER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;MAX_TOKENS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MAX_TOKENS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"400"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ThrottlingException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ServiceUnavailableException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ModelNotReadyException"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Summarize this shift's machine telemetry for a line supervisor in five bullets."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"telemetry_digest"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inferenceConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;MAX_TOKENS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# reserved against TPM at call start&lt;/span&gt;
        &lt;span class="n"&gt;serviceTier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;SERVICE_TIER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Records"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"line_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"chars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# write it wherever you store results&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Code"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;RETRYABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# Hand the message back. It reappears after the visibility timeout.&lt;/span&gt;
                &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"itemIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"messageId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"batchItemFailures"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Harbor Works API tier runs on EC2, so it gets target tracking for minute-to-minute load and a predictive policy for shift changes. With both active, the group runs at whichever capacity is higher.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_policy" rel="noopener noreferrer"&gt;aws_autoscaling_policy&lt;/a&gt; docs and the &lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/predictive-scaling-policy-overview.html" rel="noopener noreferrer"&gt;predictive scaling guide&lt;/a&gt; on Oct 29, 2026. Validated with the configuration above. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"asg_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_policy"&lt;/span&gt; &lt;span class="s2"&gt;"cpu_target"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"harbor-api-cpu-60"&lt;/span&gt;
  &lt;span class="n"&gt;autoscaling_group_name&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asg_name&lt;/span&gt;
  &lt;span class="n"&gt;policy_type&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TargetTrackingScaling"&lt;/span&gt;
  &lt;span class="n"&gt;estimated_instance_warmup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
  &lt;span class="n"&gt;target_tracking_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;predefined_metric_specification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;predefined_metric_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ASGAverageCPUUtilization"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;target_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="c1"&gt;# higher = cheaper, lower = more headroom for spikes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_policy"&lt;/span&gt; &lt;span class="s2"&gt;"shift_forecast"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"harbor-api-shift-forecast"&lt;/span&gt;
  &lt;span class="n"&gt;autoscaling_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asg_name&lt;/span&gt;
  &lt;span class="n"&gt;policy_type&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PredictiveScaling"&lt;/span&gt;
  &lt;span class="n"&gt;predictive_scaling_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ForecastOnly"&lt;/span&gt; &lt;span class="c1"&gt;# compare forecast to reality before ForecastAndScale&lt;/span&gt;
    &lt;span class="n"&gt;scheduling_buffer_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;            &lt;span class="c1"&gt;# launch 10 minutes before the forecast hour&lt;/span&gt;
    &lt;span class="n"&gt;metric_specification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;target_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
      &lt;span class="n"&gt;predefined_metric_pair_specification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;predefined_metric_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ASGCPUUtilization"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave it in &lt;code&gt;ForecastOnly&lt;/code&gt; for two weeks, compare the forecast with real load, then switch to &lt;code&gt;ForecastAndScale&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scalability in the age of AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI as a workload: tokens per minute are the unit
&lt;/h3&gt;

&lt;p&gt;Once a model sits in your request path, vCPUs stop being the number that matters. Bedrock limits you per model and per Region in &lt;strong&gt;tokens per minute&lt;/strong&gt; (TPM) and, for most models, requests per minute, with separate quotas for cross-Region and in-Region use. Increases aren't automatic, so request quota the way you'd plan instances for a launch.&lt;/p&gt;

&lt;p&gt;The quota is spent in a way that surprises people. A call starts by reserving its input tokens plus your full &lt;code&gt;max_tokens&lt;/code&gt;, then settles on input plus output tokens times a &lt;strong&gt;burndown rate&lt;/strong&gt;: 5x for Anthropic models up to version 4.7, higher for some newer ones, 1x for most others (&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-token-burndown.html" rel="noopener noreferrer"&gt;Bedrock docs&lt;/a&gt;). You're billed only for real tokens, so a careless &lt;code&gt;max_tokens&lt;/code&gt; of 32,000 throttles you long before the bill looks big. Watch the &lt;code&gt;EstimatedTPMQuotaUsage&lt;/code&gt; metric, added in March 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agents multiply load
&lt;/h3&gt;

&lt;p&gt;One user action is no longer one call. Anthropic measured its agents using about 4x the tokens of a chat interaction, and its multi-agent systems about 15x (&lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;Anthropic, 2025&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  U["Supervisor asks: why did line 2 slow down?"] --&amp;gt; A["Agent"]
  A --&amp;gt; M1["Model call 1: plan"]
  M1 --&amp;gt; T1["Tool: query telemetry"]
  M1 --&amp;gt; T2["Tool: read maintenance log"]
  T1 --&amp;gt; M2["Model call 2: read results"]
  T2 --&amp;gt; M2
  M2 --&amp;gt; T3["Tool: compare with line 1"]
  T3 --&amp;gt; M3["Model calls 3 to N:&amp;lt;br/&amp;gt;context grows each turn"]
  M3 --&amp;gt; R["One answer"]
  M3 -.-&amp;gt;|"Each call spends"| QT["TPM quota and&amp;lt;br/&amp;gt;tool API capacity"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;One question to an agent becomes N model calls and M tool calls, and every one of them counts against a quota.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's put numbers on it with Claude Sonnet 4.6 at $3.30 per million input and $16.50 per million output tokens (US geo profile, us-east-1). One call with 6,000 input and 400 output tokens costs $0.0198 + $0.0066 = $0.0264. Six such calls cost $0.158 per question and, with the 5x burndown, use 36,000 + 2,400 × 5 = 48,000 quota tokens. On a 200,000 TPM quota, that's about four questions a minute for the whole account, so cap agent turns in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bedrock capacity options, weighed by cost
&lt;/h3&gt;

&lt;p&gt;Bedrock prices &lt;strong&gt;Priority&lt;/strong&gt; at a 75% premium over Standard, &lt;strong&gt;Flex&lt;/strong&gt; at a 50% discount and &lt;strong&gt;batch&lt;/strong&gt; at 50% below on-demand for selected models (&lt;a href="https://aws.amazon.com/bedrock/pricing/" rel="noopener noreferrer"&gt;Bedrock pricing&lt;/a&gt;). The us-east-1 price list agrees, with Nova 2 Lite input at $0.33 per million on Standard, $0.165 on Flex and $0.5775 on Priority.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Priority&lt;/strong&gt; buys the front of the line for customer-facing peaks and shares the Standard quota. Support varies, and Sonnet 4.6 offers neither Priority nor Flex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flex&lt;/strong&gt; is the cheap default for anything nobody is waiting on, like the shift summaries above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch&lt;/strong&gt; halves the price of evals and backfills (Sonnet 4.6 input drops from $3.30 to $1.65). It runs from S3 files, and its Converse format doesn't support tool calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserved&lt;/strong&gt; sells a fixed TPM rate for 1 or 3 months, targets 99.5% uptime and overflows to Standard. Here's the catch. For Sonnet 4.6 (US geo, 1 month), 1,000 input TPM costs $0.198 an hour, exactly 1,000 × 60 × $3.30 per million. You break even only if you use every reserved token every minute, or 90% of them on the 10%-cheaper 3-month term. The 100,000 input plus 10,000 output TPM minimum is about $21,681 a month. Reserved buys certainty, not a discount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provisioned Throughput&lt;/strong&gt; bills model units hourly and is required for custom models. Nova 2 Lite is $55 an hour on a 1-month term, $40,150 a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Self-host on EKS, or stay on Bedrock?
&lt;/h3&gt;

&lt;p&gt;Ministral 3 8B makes a fair test, because the same Apache 2.0 open-weight model runs on Bedrock at $0.15 per million tokens (input or output) and on vLLM on your own GPU. Its model card says it fits in 12 GB of VRAM in FP8.&lt;/p&gt;

&lt;p&gt;Self-hosting needs two nodes for availability. Two &lt;code&gt;g6.xlarge&lt;/code&gt; (one 24 GB L4 GPU each) at $0.8048 an hour plus the EKS fee cost 2 × $587.50 + $73 = $1,248 a month, the price of 8.3 billion Bedrock tokens, or 3,170 tokens per second around the clock.&lt;/p&gt;

&lt;p&gt;Now add people. Say drivers, upgrades and on-call take a quarter of an engineer, and call that $4,000 a month (use your own number). Break-even jumps to $5,248 ÷ $0.15 ≈ 35 billion tokens a month, about 13,300 tokens per second sustained. Against Flex or batch at $0.07, it's 75 billion. A 1-year RI cuts the hardware to about $838, which barely moves the total.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  S["Monthly tokens on one open-weight model"] --&amp;gt; D{"Above break-even?&amp;lt;br/&amp;gt;nodes plus ops divided&amp;lt;br/&amp;gt;by Bedrock price"}
  D --&amp;gt;|"No"| BR["Stay on Bedrock&amp;lt;br/&amp;gt;Flex or batch for async"]
  D --&amp;gt;|"Yes"| U{"Load steady&amp;lt;br/&amp;gt;around the clock?"}
  U --&amp;gt;|"No, bursty"| BR
  U --&amp;gt;|"Yes"| T{"Can your nodes&amp;lt;br/&amp;gt;sustain it at your SLO?"}
  T --&amp;gt;|"Not measured"| BM["Benchmark vLLM&amp;lt;br/&amp;gt;on your prompts first"]
  T --&amp;gt;|"Yes"| SH["Self-host on EKS&amp;lt;br/&amp;gt;Karpenter, GPU or Inferentia"]
  T --&amp;gt;|"Needs more nodes"| D&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Self-hosting pays off only when your token volume is high, steady and measured on your own hardware.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last box matters. There's no published AWS benchmark for this model on L4 or &lt;code&gt;inf2.xlarge&lt;/code&gt; ($0.7582 an hour) that I can point you to, so measure it with vLLM's benchmark tooling on your own prompts. Self-host for data control or truly steady volume, not to save money at moderate scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector indexes scale on a different curve
&lt;/h3&gt;

&lt;p&gt;Retrieval scales on storage and query rate, not tokens. S3 Vectors holds up to 2 billion vectors per index at $0.06 per GB-month plus $2.50 per million queries and a small processing fee. AWS's own example, 10 million vectors and a million queries a month, costs $11.38, with frequent queries around 100 ms. OpenSearch Serverless is faster and richer. A first classic collection bills at least 2 OCUs at $0.24 per OCU-hour, about $350 a month before any data. The newer NextGen collections have no minimum and scale to zero after 10 idle minutes.&lt;/p&gt;

&lt;p&gt;One limit catches people out. An S3 Vectors index takes at most 2,500 inserts and deletes per second, so re-embedding 10 million vectors into one index takes at least 67 minutes. Spread big re-embeds across indexes, and keep the hot set users query all day in OpenSearch.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;p&gt;AI assistants are good at the tedious parts. They draft a Terraform policy, write a k6 script or explain a throttling graph, and AWS DevOps Agent (generally available since March 2026) works on resolving and preventing incidents.&lt;/p&gt;

&lt;p&gt;They're weak on exactly the numbers this article depends on. Expect last year's prices, invented default quotas, and CPU target tracking for a workload that waits on I/O. So pull prices from the Price List API, read quotas from Service Quotas, and treat an AI-written scaling policy as untested until a load test has pushed past its target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who: owners and the shared responsibility split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Product owner&lt;/th&gt;
&lt;th&gt;Architect&lt;/th&gt;
&lt;th&gt;Developers&lt;/th&gt;
&lt;th&gt;Platform / SRE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scaling unit, unit-cost target, growth forecast&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ceiling analysis and compute choice&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless code, pooling, idempotency, agent turn caps&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling policies, quotas, load tests, cost dashboards&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;R = responsible, A = accountable, C = consulted, I = informed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For compute, the "AWS handles" and "You still own" columns in the Decide table are the split. For managed AI, AWS serves the model, honors your tier and enforces quotas. You pick the model and tier, request quota early, set &lt;code&gt;max_tokens&lt;/code&gt; and cap agent fan-out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pick your scaling unit and put cost per unit of work on your main dashboard.&lt;/li&gt;
&lt;li&gt;[ ] Find your lowest ceiling: database connections, a hot partition key, or a model quota.&lt;/li&gt;
&lt;li&gt;[ ] Put RDS Proxy in front of any relational database that Lambda or autoscaled containers talk to.&lt;/li&gt;
&lt;li&gt;[ ] Work out where Lambda and always-on capacity break even for your busiest service, using this year's prices.&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;MaximumConcurrency&lt;/code&gt; to every SQS event source that calls something with a hard limit.&lt;/li&gt;
&lt;li&gt;[ ] Set &lt;code&gt;max_tokens&lt;/code&gt; close to real output sizes and alarm on &lt;code&gt;EstimatedTPMQuotaUsage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Move evals, backfills and summaries to Flex or batch.&lt;/li&gt;
&lt;li&gt;[ ] Count model and tool calls per user action for each agent, and cap the turns in code.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Every option here scales. They differ on the price of each extra unit. Lambda charges more per busy hour and nothing for idle time, and containers flip that. Reserved model capacity buys certainty at the full on-demand rate. Self-hosting buys control that pays off only at volumes most teams never reach.&lt;/p&gt;

&lt;p&gt;So start with the ceiling, because no amount of compute fixes a database at its connection limit or an agent that spends a whole minute of quota on four questions. Then price the unit and pick the option whose cost curve matches your load. Prime Video didn't need faster services. It needed a design whose cost grew with the work instead of ahead of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale is easy to buy. Find the ceiling and price the unit before you do.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Availability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Maintainability (coming soon)&lt;/li&gt;
&lt;li&gt;Cost (coming soon)&lt;/li&gt;
&lt;li&gt;Sustainability (coming soon)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up, Maintainability:&lt;/strong&gt; the pitfalls that make a system harder to change every month, and what CodeBuild, CodeDeploy and CloudWatch can and can't do about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.infoq.com/news/2023/05/prime-ec2-ecs-saves-costs/" rel="noopener noreferrer"&gt;Prime Video swaps microservices for monolith: 90% cost reduction — Rafał Gancarz, InfoQ, May 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://business.ticketmaster.com/press-release/taylor-swift-the-eras-tour-onsale-explained/" rel="noopener noreferrer"&gt;Taylor Swift | The Eras Tour onsale explained — Ticketmaster, November 19, 2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.globenewswire.com/de/news-release/2026/03/18/3258065/0/en/flexera-finds-cloud-value-is-rising-while-ai-waste-grows.html" rel="noopener noreferrer"&gt;Flexera finds cloud value is rising while AI waste grows — Flexera 2026 State of the Cloud, March 18, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/aws-services-scale-to-new-heights-for-prime-day-2025-key-metrics-and-milestones" rel="noopener noreferrer"&gt;AWS services scale to new heights for Prime Day 2025 — Channy Yun, AWS News Blog, August 26, 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json" rel="noopener noreferrer"&gt;AWS Price List API — bulk offer files for AWSLambda, AmazonECS, AmazonEKS, AmazonEC2, AmazonRDS, AWSQueueService and AmazonBedrock, us-east-1, retrieved October 29, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html" rel="noopener noreferrer"&gt;Quotas for Amazon RDS (max_connections defaults) — RDS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html" rel="noopener noreferrer"&gt;Amazon RDS Proxy — RDS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html" rel="noopener noreferrer"&gt;Replication with Amazon Aurora — Aurora docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless.html" rel="noopener noreferrer"&gt;Amazon Aurora PostgreSQL Limitless Database — Aurora docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/10/amazon-aurora-postgresql-limitless-database-generally-available" rel="noopener noreferrer"&gt;Amazon Aurora PostgreSQL Limitless Database is now generally available — AWS, October 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html" rel="noopener noreferrer"&gt;Best practices for designing and using partition keys — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-sharding.html" rel="noopener noreferrer"&gt;Using write sharding to distribute workloads evenly — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ServiceQuotas.html" rel="noopener noreferrer"&gt;Service, account, and table quotas in Amazon DynamoDB — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html" rel="noopener noreferrer"&gt;Understanding Lambda function scaling — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html" rel="noopener noreferrer"&gt;Configure Lambda function memory — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html" rel="noopener noreferrer"&gt;Lambda Managed Instances — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;AWS Lambda pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html" rel="noopener noreferrer"&gt;Configuring scaling behavior for SQS event source mappings — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html" rel="noopener noreferrer"&gt;Creating an SQS event source mapping — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/08/aws-Lambda-provisioned-sqs-esm-max-pollers/" rel="noopener noreferrer"&gt;AWS Lambda Provisioned Mode for SQS event source mappings now supports up to 10,000 event pollers — AWS, August 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/" rel="noopener noreferrer"&gt;Amazon SQS pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-scaling-target-tracking.html" rel="noopener noreferrer"&gt;Target tracking scaling policies for Amazon EC2 Auto Scaling — EC2 Auto Scaling docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/predictive-scaling-policy-overview.html" rel="noopener noreferrer"&gt;How predictive scaling works — EC2 Auto Scaling docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/autoscaling/pricing/" rel="noopener noreferrer"&gt;Amazon EC2 Auto Scaling pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html" rel="noopener noreferrer"&gt;Spot Instance interruption notices — EC2 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/savingsplans/compute-pricing/" rel="noopener noreferrer"&gt;Compute Savings Plans pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/fargate/pricing/" rel="noopener noreferrer"&gt;AWS Fargate pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-autoscaling-targettracking.html" rel="noopener noreferrer"&gt;Target tracking for Amazon ECS services — ECS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://karpenter.sh/docs/concepts/nodepools/" rel="noopener noreferrer"&gt;Karpenter NodePools — karpenter.sh (v1.15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://karpenter.sh/docs/concepts/nodeclasses/" rel="noopener noreferrer"&gt;Karpenter NodeClasses — karpenter.sh (v1.15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://keda.sh/docs/latest/scalers/aws-sqs/" rel="noopener noreferrer"&gt;KEDA AWS SQS Queue scaler — keda.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/ml-realtime-inference-llm-inference-vllm.html" rel="noopener noreferrer"&gt;Quickstart: High-throughput LLM inference with vLLM on Amazon EKS — EKS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/serving-llms-using-vllm-and-amazon-ec2-instances-with-aws-ai-chips/" rel="noopener noreferrer"&gt;Serving LLMs using vLLM and Amazon EC2 instances with AWS AI chips — AWS Machine Learning Blog, November 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/mistralai/Ministral-3-8B-Instruct-2512" rel="noopener noreferrer"&gt;Ministral-3-8B-Instruct-2512 model card — Mistral AI, Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html" rel="noopener noreferrer"&gt;Quotas for Amazon Bedrock — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-runtime.html" rel="noopener noreferrer"&gt;bedrock-runtime quotas — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-token-burndown.html" rel="noopener noreferrer"&gt;Token burndown and quotas — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-observability-ttft-quota" rel="noopener noreferrer"&gt;Amazon Bedrock now supports observability of First Token Latency and Quota Consumption — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html" rel="noopener noreferrer"&gt;Service tiers for model inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/pricing/" rel="noopener noreferrer"&gt;Amazon Bedrock pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/batch-inference.html" rel="noopener noreferrer"&gt;Process multiple prompts with batch inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prov-throughput.html" rel="noopener noreferrer"&gt;Provisioned Throughput — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Converse — Amazon Bedrock API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html" rel="noopener noreferrer"&gt;Amazon Nova 2 Lite model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-4-6.html" rel="noopener noreferrer"&gt;Claude Sonnet 4.6 model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;How we built our multi-agent research system — Anthropic Engineering, June 13, 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-s3-vectors-generally-available/" rel="noopener noreferrer"&gt;Amazon S3 Vectors is now generally available — AWS, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors-limitations.html" rel="noopener noreferrer"&gt;S3 Vectors limitations and restrictions — S3 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/pricing/" rel="noopener noreferrer"&gt;Amazon S3 pricing (S3 Vectors)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/opensearch-service/pricing/" rel="noopener noreferrer"&gt;Amazon OpenSearch Service pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-devops-agent-generally-available" rel="noopener noreferrer"&gt;AWS DevOps Agent is now generally available — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability" rel="noopener noreferrer"&gt;AWS service availability updates — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_lambda_event_source_mapping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_policy" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_autoscaling_policy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>scalability</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Availability</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 11 Nov 2026 15:01:56 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-availability-3hgg</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-availability-3hgg</guid>
      <description>&lt;p&gt;At 11:48 PM Pacific on October 19, 2025, two copies of the automation that manages DynamoDB's DNS in us-east-1 raced each other and left the Regional endpoint with an empty DNS record. DNS was back by 2:25 AM, but the outage wasn't over. EC2's host-management system depends on DynamoDB and fell into "congestive collapse". New instances came up before their network state had propagated, so load balancer health checks pulled healthy capacity out of service. AWS put the end at 2:20 PM, more than 14 hours in (&lt;a href="https://aws.amazon.com/message/101925/" rel="noopener noreferrer"&gt;AWS post-event summary, 2025&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;One line in that summary deserves more attention. EC2 instances launched before the event "remained healthy" throughout. The systems that rode it out already had what they needed, and that idea runs through this article.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Promise an &lt;strong&gt;SLO with an error budget&lt;/strong&gt;, never "zero downtime". At 99.9% over 30 days you get 43 minutes.&lt;/li&gt;
&lt;li&gt;Your ceiling is the &lt;strong&gt;product&lt;/strong&gt; of your hard dependencies. Copies only help if they fail independently.&lt;/li&gt;
&lt;li&gt;Design for &lt;strong&gt;static stability&lt;/strong&gt;: recover with capacity you already have, not a control plane mid-incident.&lt;/li&gt;
&lt;li&gt;Let &lt;strong&gt;RTO and RPO&lt;/strong&gt; pick your DR strategy, then rehearse it with AWS FIS.&lt;/li&gt;
&lt;li&gt;For AI, treat 429s, 503s and model end-of-life as outages, and build a &lt;strong&gt;degradation ladder&lt;/strong&gt; whose last rung needs no model.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What: availability, and the words people swap for it
&lt;/h2&gt;

&lt;p&gt;Availability asks one question. Can users get the function they were promised, right now? &lt;strong&gt;Durability&lt;/strong&gt; asks whether the data still exists, and &lt;strong&gt;resilience&lt;/strong&gt; asks how well the system absorbs a hit and recovers. S3 Standard is designed for 99.99% availability and eleven nines of durability, so a bucket you can't reach for ten minutes hasn't lost a byte.&lt;/p&gt;

&lt;p&gt;Four terms make it testable. An &lt;strong&gt;SLI&lt;/strong&gt; is good events over total events, such as &lt;code&gt;/orders&lt;/code&gt; requests answered without a 5xx in under a second. An &lt;strong&gt;SLO&lt;/strong&gt; is the target for that ratio, say 99.9% over 30 days. An &lt;strong&gt;SLA&lt;/strong&gt; is the contract with credits, set below your SLO. The &lt;strong&gt;error budget&lt;/strong&gt; is what's left, so 3 million requests at 99.9% allow 3,000 failures (Google SRE Workbook).&lt;/p&gt;

&lt;p&gt;Each nine divides your allowance by ten. Over 30 days, 99.9% gives you 43 minutes, 99.99% about 4, and 99.999% just 26 seconds.&lt;/p&gt;

&lt;p&gt;Part 1 of this series promised "zero downtime, no matter what." I'm taking that back. What you can honestly promise is an SLO, a budget, and a plan for when the budget runs out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: downtime has a price, and someone else often sets it
&lt;/h2&gt;

&lt;p&gt;The October outage drew more than 17 million Downdetector reports, the most of any outage in 2025 (Ookla, December 2025). In Uptime Institute's 2025 survey, one in five operators said their last major outage cost over $1 million (Uptime Institute, Annual Outage Analysis 2026).&lt;/p&gt;

&lt;p&gt;The number I'd put on a slide is from the same report. Over nine years of publicly reported outages, third parties such as cloud, telecom and colocation providers caused about two-thirds. Part of your availability is somebody else's architecture, so design as if it will fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The theory: four models for your next design review
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Error budgets turn "be reliable" into numbers
&lt;/h3&gt;

&lt;p&gt;While budget remains, you ship. Once it's spent, a written &lt;strong&gt;error budget policy&lt;/strong&gt; kicks in, usually freezing risky changes until you're back within SLO. Alert on &lt;strong&gt;burn rate&lt;/strong&gt;, how fast the budget is going, not on single errors. Google's SRE Workbook suggests paging when an hour eats 2% of a 30-day budget, a burn rate of 14.4.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dependencies multiply, and copies only add if they're independent
&lt;/h3&gt;

&lt;p&gt;Hard dependencies sit in series, so their availabilities multiply: &lt;code&gt;A1 × A2 × … × An&lt;/code&gt;. Redundant copies sit in parallel, so all of them must fail at once: &lt;code&gt;1 − (1 − A)^n&lt;/code&gt; (AWS, Availability and Beyond whitepaper).&lt;/p&gt;

&lt;p&gt;Let's put numbers on it with Harbor Works, the fictional plant in my open-source app Luso. Its line copilot runs API Gateway → Lambda → DynamoDB, then calls Amazon Bedrock. With the published SLAs (API Gateway and Lambda 99.95%, DynamoDB 99.99%, Bedrock 99.9%):&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  AG["API Gateway&amp;lt;br/&amp;gt;99.95%"] --&amp;gt; LA["Lambda&amp;lt;br/&amp;gt;99.95%"]
  LA --&amp;gt; DB["DynamoDB&amp;lt;br/&amp;gt;99.99%"]
  DB --&amp;gt; S["Serverless stack ceiling&amp;lt;br/&amp;gt;99.89%, about 48 min a month"]
  S --&amp;gt; BR["Bedrock&amp;lt;br/&amp;gt;99.9%"]
  BR --&amp;gt; C["Copilot ceiling&amp;lt;br/&amp;gt;99.79%, about 91 min a month"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Each hard dependency in series lowers the ceiling, and the model call alone takes it from about 48 to about 91 minutes a month.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The serverless stack alone is 0.9995 × 0.9995 × 0.9999 = &lt;strong&gt;99.89%&lt;/strong&gt;, about 48 minutes a month.&lt;/li&gt;
&lt;li&gt;Add Bedrock in series and you get 99.89% × 99.9% = &lt;strong&gt;99.79%&lt;/strong&gt;, about 91 minutes.&lt;/li&gt;
&lt;li&gt;Copy everything to a second Region with its own Bedrock: 1 − (1 − 0.9979)² = &lt;strong&gt;99.9996%&lt;/strong&gt;, about 11 seconds.&lt;/li&gt;
&lt;li&gt;Copy the stack but keep both Regions calling Bedrock in one Region: (1 − (1 − 0.9989)²) × 0.999 = &lt;strong&gt;99.90%&lt;/strong&gt;, about 43 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare the last two. Both run in two Regions for a similar bill, yet one shared dependency throws away three nines. A shared deploy pipeline or identity provider does the same, just more quietly.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  U["User"] --&amp;gt; R53["Route 53 DNS"]
  R53 --&amp;gt; A["Region A stack&amp;lt;br/&amp;gt;99.89%"]
  R53 --&amp;gt; B["Region B stack&amp;lt;br/&amp;gt;99.89%"]
  A --&amp;gt; X["Shared dependency&amp;lt;br/&amp;gt;model Region, deploy pipeline, identity"]
  B --&amp;gt; X&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Two Region stacks only count as parallel when nothing like the box on the right sits behind both of them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Treat this as a ceiling, not a forecast. AWS calls the calculation "a rough order of magnitude." It's still the fastest way I know to catch a design that can't meet its SLO even on paper.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Decide how much is allowed to break at once
&lt;/h3&gt;

&lt;p&gt;Blast radius is a choice you make in advance. AZs and Regions are boundaries AWS gives you. &lt;strong&gt;Cells&lt;/strong&gt; are ones you build: isolated copies of the workload, each serving a slice of traffic by partition key. With 10 cells, one failure leaves 90% of requests untouched (AWS cell-based architecture guide). Keep the router "the thinnest possible layer", split on a natural key (for Harbor Works, the production line), and deploy one cell at a time.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  T["Requests"] --&amp;gt; RT["Cell router&amp;lt;br/&amp;gt;thinnest possible layer&amp;lt;br/&amp;gt;splits on production line"]
  RT --&amp;gt; C1["Cell 1&amp;lt;br/&amp;gt;isolated copy of the workload"]
  RT --&amp;gt; C2["Cell 2&amp;lt;br/&amp;gt;failed"]
  RT --&amp;gt; C3["Cells 3 to 10&amp;lt;br/&amp;gt;isolated copies of the workload"]
  C1 --&amp;gt; OK["90% of requests untouched"]
  C3 --&amp;gt; OK
  C2 --&amp;gt; BAD["10% of requests affected"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;With ten cells, a failure stays inside one cell's slice of traffic instead of reaching every user.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Static stability: recover without asking for anything new
&lt;/h3&gt;

&lt;p&gt;"In a statically stable design, the overall system keeps working even when a dependency becomes impaired" (&lt;a href="https://builder.aws.com/content/3K9SiqiJ369TGkkGzqPM95BuYWI/static-stability-using-availability-zones" rel="noopener noreferrer"&gt;Mike Furr, Amazon Builders' Library, 2019&lt;/a&gt;). The &lt;strong&gt;control plane&lt;/strong&gt; creates and changes resources. The &lt;strong&gt;data plane&lt;/strong&gt; keeps existing ones working, and it's simpler and built for higher availability. October was the textbook case: running instances kept serving while launches failed.&lt;/p&gt;

&lt;p&gt;Three rules follow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-provision across AZs.&lt;/strong&gt; If peak needs 10 instances, run 15 across three AZs so losing a zone needs no launches. AWS's DR whitepaper is blunt about why: "Auto Scaling is a control plane activity".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail over with data-plane actions.&lt;/strong&gt; Route 53's control plane lives in us-east-1, while its data plane answers from over 200 points of presence. Health-check failover is data plane. Editing a record mid-incident isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make automatic failover cautious.&lt;/strong&gt; After October, AWS is adding "a velocity control mechanism to limit the capacity a single NLB can remove." Keep your own health checks shallow, because a deep check on a shared dependency turns one failure into a cascade.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  I["Incident&amp;lt;br/&amp;gt;an AZ or a dependency is impaired"] --&amp;gt; CP["Recovery that asks the control plane"]
  I --&amp;gt; DP["Recovery on the data plane"]
  CP --&amp;gt; CP1["Launch replacement instances&amp;lt;br/&amp;gt;Auto Scaling"]
  CP --&amp;gt; CP2["Edit a Route 53 record"]
  CP1 --&amp;gt; R1["Can fail mid-incident&amp;lt;br/&amp;gt;as launches did in October"]
  CP2 --&amp;gt; R1
  DP --&amp;gt; DP1["Pre-provisioned capacity&amp;lt;br/&amp;gt;15 instances across 3 AZs for a peak of 10"]
  DP --&amp;gt; DP2["Route 53 health-check failover"]
  DP1 --&amp;gt; R2["Keeps working with&amp;lt;br/&amp;gt;what already exists"]
  DP2 --&amp;gt; R2&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;A statically stable recovery only uses what's already running, so it works even while the control plane is struggling.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When: where availability enters the lifecycle
&lt;/h2&gt;

&lt;p&gt;At design time, set one SLO per journey and mark every dependency hard or soft. In the build, put a timeout on every call, retries in one layer and idempotent writes. Before launch, run one fault-injection experiment per failure mode and one real failover. In operation, alarm on burn rate and drill on a calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where: the AWS pieces, and when not to use them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route 53 health checks with failover records&lt;/strong&gt; give you data-plane Regional failover in about 90 seconds at the default settings, plus the 60-second TTL on ALB DNS names. A check on an AWS endpoint costs $0.50 a month, plus $1 per optional feature such as HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Application Recovery Controller (ARC)&lt;/strong&gt; zonal shift and autoshift move traffic off an impaired AZ. Autoshift needs weekly practice runs of about 30 minutes, so pre-scale the other AZs first. ARC Region switch runs multi-Region recovery plans with "a data plane in each AWS Region". ARC readiness check closed to new customers on April 30, 2026, and AWS points you to Region switch instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora Global Database&lt;/strong&gt; has RPO 0 on a planned switchover, but an unplanned failover loses seconds of data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB global tables&lt;/strong&gt; (99.999% SLA) in default MREC mode have an RPO of a few seconds. MRSC gets RPO 0 but needs exactly three Regions and drops TTL, local secondary indexes and transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS FIS&lt;/strong&gt; runs the experiments, and its scenario library includes &lt;em&gt;AZ Availability: Power Interruption&lt;/em&gt; and &lt;em&gt;Cross-Region: Connectivity&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For experiments inside Kubernetes, LitmusChaos (CNCF Incubating) fits better, and Sloth generates Prometheus SLO rules from a short spec. For FIS across many accounts, see my &lt;a href="https://dev.to/aws-builders/conquering-cloud-chaos-multi-account-experiments-with-aws-fis-injector-19ji"&gt;Conquering Cloud Chaos (2024)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: how far to go, and what it costs elsewhere
&lt;/h2&gt;

&lt;p&gt;Start with two numbers per journey. &lt;strong&gt;RTO&lt;/strong&gt; is the longest acceptable gap before service comes back. &lt;strong&gt;RPO&lt;/strong&gt; is how much data, measured in time, you can afford to lose. Write both down before anyone says "multi-Region", then pick the cheapest of AWS's four strategies that meets them (Well-Architected REL13-BP02).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5jptnmjtsih3ppqdevmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5jptnmjtsih3ppqdevmu.png" alt="Four disaster recovery strategies, from cheapest to most expensive. Backup and restore loses hours of data and takes up to a day to recover. Pilot light keeps data replicas with compute switched off, losing minutes and recovering in tens of minutes. Warm standby runs a scaled-down full stack, losing seconds and recovering in minutes. Active-active serves live traffic from every Region for near-zero loss, at the highest cost and complexity." width="799" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;RPO / RTO (per AWS)&lt;/th&gt;
&lt;th&gt;What runs in the recovery Region&lt;/th&gt;
&lt;th&gt;Choose it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backup and restore&lt;/td&gt;
&lt;td&gt;Hours / 24 h or less&lt;/td&gt;
&lt;td&gt;Backups only&lt;/td&gt;
&lt;td&gt;Internal tools, or data you can rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pilot light&lt;/td&gt;
&lt;td&gt;Minutes / tens of minutes&lt;/td&gt;
&lt;td&gt;Data replicas, compute switched off&lt;/td&gt;
&lt;td&gt;Data must be current, but an hour down is survivable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm standby&lt;/td&gt;
&lt;td&gt;Seconds / minutes&lt;/td&gt;
&lt;td&gt;A scaled-down full stack&lt;/td&gt;
&lt;td&gt;Customer-facing journeys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-site active-active&lt;/td&gt;
&lt;td&gt;Near zero / potentially zero&lt;/td&gt;
&lt;td&gt;Full stack taking live traffic&lt;/td&gt;
&lt;td&gt;Revenue stops when you stop, and the team can run it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My rule of thumb is multi-AZ in one Region for SLOs up to about 99.95%, and multi-Region at 99.99% or when a regulator wants a separate site. Bad writes replicate too, so even active-active needs point-in-time backups you've actually restored.&lt;/p&gt;

&lt;p&gt;Every extra nine gets paid for somewhere else in this series.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Other NFR&lt;/th&gt;
&lt;th&gt;What more availability does to it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;More Regions mean more IAM, keys and secrets to keep in sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;Replicas and cross-Region inference move where data lives and is processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Fallback and "degraded" pages are often the least-tested UI you ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;MRSC and synchronous replication add write latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Static stability pre-buys capacity instead of scaling on demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Each Region is another deploy target, and drift is what breaks failovers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Warm standby and active-active mean paying for a second data tier that mostly waits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustainability&lt;/td&gt;
&lt;td&gt;Idle standby capacity draws power every hour it waits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hands-on: data-plane failover, then break it on purpose
&lt;/h2&gt;

&lt;p&gt;Harbor Works serves from eu-west-1 with a warm standby in eu-central-1, both behind existing ALBs. Everything exists ahead of time, so failover makes no control-plane calls.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/Route53/latest/APIReference/API_HealthCheckConfig.html" rel="noopener noreferrer"&gt;Route 53 HealthCheckConfig API reference&lt;/a&gt; and the Terraform AWS provider docs on Oct 29, 2026. &lt;code&gt;terraform validate&lt;/code&gt; passes on Terraform 1.17.1 with AWS provider 6.71.0. Not deployed, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"zone_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"record_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"primary_alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-euw1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# existing ALB names&lt;/span&gt;
&lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"secondary_alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-euc1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secondary"&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-central-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_zone"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary_alb&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"secondary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary_alb&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Route 53 checkers probe this from several locations. No API call happens at failover time.&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_health_check"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
  &lt;span class="n"&gt;fqdn&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dns_name&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTPS"&lt;/span&gt;
  &lt;span class="n"&gt;port&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
  &lt;span class="n"&gt;resource_path&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/healthz"&lt;/span&gt;                              &lt;span class="c1"&gt;# shallow: this Region's own deps only&lt;/span&gt;
  &lt;span class="n"&gt;request_interval&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;                                      &lt;span class="c1"&gt;# 10 is faster, costs extra, can't change later&lt;/span&gt;
  &lt;span class="n"&gt;failure_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;                                       &lt;span class="c1"&gt;# about 90 s to mark unhealthy&lt;/span&gt;
  &lt;span class="n"&gt;regions&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# API minimum is 3&lt;/span&gt;
  &lt;span class="n"&gt;tags&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-primary-euw1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
  &lt;span class="n"&gt;zone_id&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_name&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;
  &lt;span class="n"&gt;set_identifier&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"primary-euw1"&lt;/span&gt;
  &lt;span class="n"&gt;health_check_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_route53_health_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="n"&gt;failover_routing_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PRIMARY"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dns_name&lt;/span&gt;
    &lt;span class="n"&gt;zone_id&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;
    &lt;span class="n"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# also fail over if the ALB has no healthy targets&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"secondary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
  &lt;span class="n"&gt;zone_id&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_name&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;
  &lt;span class="n"&gt;set_identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secondary-euc1"&lt;/span&gt;
  &lt;span class="n"&gt;failover_routing_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SECONDARY"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dns_name&lt;/span&gt;
    &lt;span class="n"&gt;zone_id&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;
    &lt;span class="n"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One Route 53 behavior surprises people: if both records are unhealthy, it returns the primary anyway.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant HC as Health checkers
  participant P as Primary ALB
  participant DNS as Route 53 DNS
  participant C as Client
  HC-&amp;gt;&amp;gt;P: GET /healthz every 30 s
  P--&amp;gt;&amp;gt;HC: 3 consecutive failures
  HC-&amp;gt;&amp;gt;DNS: primary marked unhealthy
  C-&amp;gt;&amp;gt;DNS: resolve api.example.com
  DNS--&amp;gt;&amp;gt;C: secondary ALB, TTL 60 s
  Note over C,DNS: Clients with a cached answer keep hitting the primary until the TTL expires&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Budget about 90 seconds to detect plus 60 seconds of cached DNS, and put that in your RTO.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now rehearse October in miniature. This experiment cuts the app subnets off from the Regional DynamoDB endpoint for 10 minutes, and stops early if the SLO fast-burn alarm fires.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/fis-actions-reference.html" rel="noopener noreferrer"&gt;AWS FIS actions reference&lt;/a&gt; on Oct 29, 2026, and validated against the &lt;code&gt;CreateExperimentTemplate&lt;/code&gt; model in boto3 1.43.121. Not deployed, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Game day: app subnets lose the Regional DynamoDB endpoint for 10 minutes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"appSubnets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"aws:ec2:subnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"resourceTags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"selectionMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ALL"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"denyDynamoDB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"actionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"aws:network:disrupt-connectivity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"PT10M"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"Subnets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"appSubnets"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"stopConditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"aws:cloudwatch:alarm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:cloudwatch:eu-west-1:111122223333:alarm:orders-slo-fast-burn"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"roleArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::111122223333:role/fis-experiment-role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb-endpoint-loss"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach the &lt;code&gt;AWSFaultInjectionSimulatorNetworkAccess&lt;/code&gt; managed policy to the role, then run &lt;code&gt;aws fis create-experiment-template --cli-input-json file://dynamodb-endpoint-loss.json&lt;/code&gt;. A passing run is boring. &lt;code&gt;/healthz&lt;/code&gt; stays green and users get a degraded answer, not an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability in the age of AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI as a workload
&lt;/h3&gt;

&lt;p&gt;A model is a dependency with an SLA, a quota, a lifecycle, and one failure mode your load balancer can't see: a confident wrong answer with HTTP 200.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throttling arrives long before Region outages do.&lt;/strong&gt; Bedrock's &lt;code&gt;ThrottlingException&lt;/code&gt; (429) means you hit your account quota, which an increase fixes. Its 503 means the service is short of capacity, "not related to your account-level quotas". Back off with jitter and retry in one layer only, since three layers of three attempts can hit a struggling dependency 27 times. Keep &lt;code&gt;maxTokens&lt;/code&gt; near your real output size, because Bedrock reserves it against your tokens-per-minute quota at request start. Then add one circuit breaker per model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put the model in your math.&lt;/strong&gt; The Bedrock SLA promises 99.9% for the service, but the Reserved tier "targets 99.5% uptime for model response" (&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-bedrock-reserved-service-tier" rel="noopener noreferrer"&gt;AWS, November 2025&lt;/a&gt;). Plan on the lower, model-level number. In series with Harbor Works' 99.89% stack, the ceiling drops to 99.39%, about 4.4 hours a month, and no Lambda tuning gets you back above it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-Region inference trades residency for capacity.&lt;/strong&gt; A geographic profile such as &lt;code&gt;eu.&lt;/code&gt; can process your prompt in any Region in that geography, and a &lt;code&gt;global.&lt;/code&gt; profile in any supported commercial Region, for about 10% less. Your SCPs must allow every destination, and CloudTrail records where each request ran in &lt;code&gt;additionalEventData.inferenceRegion&lt;/code&gt;. Just don't call it DR. AWS says it "does not, however, function as a disaster recovery or automatic failover system" (&lt;a href="https://aws.amazon.com/blogs/machine-learning/optimize-your-applications-for-scale-and-reliability-on-amazon-bedrock/" rel="noopener noreferrer"&gt;AWS Machine Learning Blog, February 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a degradation ladder&lt;/strong&gt;, where each rung depends on less than the one above.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  Q["User request"] --&amp;gt; R1{"Rung 1&amp;lt;br/&amp;gt;primary model"}
  R1 --&amp;gt;|"ok"| A["Full answer"]
  R1 --&amp;gt;|"429, 503, timeout or breaker open"| R2{"Rung 2&amp;lt;br/&amp;gt;smaller model, other provider"}
  R2 --&amp;gt;|"ok"| B["Shorter answer"]
  R2 --&amp;gt;|"fails"| R3{"Rung 3&amp;lt;br/&amp;gt;cached answer"}
  R3 --&amp;gt;|"hit"| C["Last good answer"]
  R3 --&amp;gt;|"miss"| R4["Rung 4&amp;lt;br/&amp;gt;non-AI path"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Rungs 1 and 2 both depend on Bedrock, so only rungs 3 and 4 survive a Bedrock-wide problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the parallel math won't rescue you. Two independent 99.5% models would give 99.9975%, but these share an endpoint, a Region and an account. Rarely used fallbacks also tend to fail when needed (Jacob Gabrielson, Amazon Builders' Library, 2019), so send a trickle of live traffic down rung 2 and test rung 4 synthetically.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Bedrock Converse API reference&lt;/a&gt; and the Claude Sonnet 4.6 and Nova 2 Lite model cards on Oct 29, 2026. Unit-tested with botocore's Stubber on boto3 1.43.121 for the throttle, 503, model-timeout and validation-error paths. Not deployed, so test it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;ClientError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ConnectTimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ReadTimeoutError&lt;/span&gt;

&lt;span class="c1"&gt;# Standard mode: exponential backoff with jitter, plus a retry quota.&lt;/span&gt;
&lt;span class="c1"&gt;# Keep attempts low. The ladder below is the real fallback.&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"max_attempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;connect_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;brt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-runtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Pinned IDs. The eu. geographic profiles keep processing inside the EU.&lt;/span&gt;
&lt;span class="no"&gt;RUNGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"eu.anthropic.claude-sonnet-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# rung 1: primary model&lt;/span&gt;
    &lt;span class="s2"&gt;"eu.amazon.nova-2-lite-v1:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# rung 2: smaller model, different provider&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ThrottlingException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ServiceUnavailableException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="s2"&gt;"ModelNotReadyException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ModelTimeoutException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="s2"&gt;"InternalServerException"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Breaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Opens after N straight failures, lets a trial call through after cooldown."""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cooldown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cooldown&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt;  &lt;span class="c1"&gt;# half-open&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;breakers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;RUNGS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# stand-in: use DynamoDB or ElastiCache, keyed by a normalized prompt&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;RUNGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                                 &lt;span class="c1"&gt;# breaker open: skip this rung fast&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;inferenceConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# reserved against TPM at request start&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"rung"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Code"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;RETRYABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;                                &lt;span class="c1"&gt;# a 400 or 403 is a bug, not an outage&lt;/span&gt;
            &lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConnectTimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ReadTimeoutError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                              &lt;span class="c1"&gt;# rung 3: last good answer&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"rung"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"rung"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"non-ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# rung 4: the product still works&lt;/span&gt;
            &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Line summaries are busy. Here is the shift log for this line."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pin model versions and watch the calendar.&lt;/strong&gt; Bedrock models go Active → Legacy → End-of-Life. After EOL "requests made to it will fail", and "migration will not happen automatically" (&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html" rel="noopener noreferrer"&gt;Bedrock model lifecycle&lt;/a&gt;). Legacy periods run six months, sometimes only 45 days. Claude Sonnet 4.5, for example, is Legacy with EOL on April 8, 2027. Pin exact IDs, pull &lt;code&gt;modelLifecycle.endOfLifeTime&lt;/code&gt; from &lt;code&gt;GetFoundationModel&lt;/code&gt; into an inventory, and alarm 90 days out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run evals as synthetic health checks.&lt;/strong&gt; A model can get worse without a single 5xx. Run a small golden set every few minutes, publish the pass rate as a CloudWatch metric, and alarm on it. A Route 53 &lt;code&gt;CLOUDWATCH_METRIC&lt;/code&gt; health check can then fail over on quality (same-account, standard-resolution alarms only).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give agents durable state.&lt;/strong&gt; An agent that forgets everything at step 31 of 40 is down. Step Functions Standard workflows and Lambda durable functions both checkpoint progress and run for up to a year. AgentCore Runtime microVM sessions default to an 8-hour lifetime and a 15-minute idle timeout, so keep state outside them, and make tool calls idempotent so a replay can't order twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector stores need an RTO and RPO too.&lt;/strong&gt; Either replicate the index, or replicate the sources and re-embed, which is cheap to keep ready but slow to run. OpenSearch Service offers active-passive cross-cluster replication. DynamoDB vector search (GA August 2026) replicates the index definition with global tables, but index contents propagate asynchronously, even under MRSC. The S3 Vectors guide documents no replication or backup as of October 2026, so replicate the source documents and count re-embedding in your RTO. Only replicate into Regions your residency rules allow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Still immature:&lt;/strong&gt; FIS has no Bedrock fault action, so simulate 429s in your client or a gateway. For provider-neutral fallback, the open-source LiteLLM proxy handles retries, cooldowns and cross-provider failover.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;p&gt;AI tools are good at the tedious parts, like drafting a dependency map from your IaC or turning a runbook into an FIS template. AWS DevOps Agent (GA March 2026) correlates telemetry, code and deployments during incidents, though its "hours to minutes" MTTR claim comes with no published measurement.&lt;/p&gt;

&lt;p&gt;They also make things up, like a Bedrock fault action that doesn't exist, or two copies in one AZ treated as independent. Validate generated templates against the API model (botocore's &lt;code&gt;ParamValidator&lt;/code&gt; checked the FIS template above), refuse experiments without a stop condition, and keep a human approval on any production failover an agent proposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who: owners and the shared responsibility split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Product owner&lt;/th&gt;
&lt;th&gt;Architect&lt;/th&gt;
&lt;th&gt;Developers&lt;/th&gt;
&lt;th&gt;SRE / platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SLOs and error budget policy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency map and DR strategy&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A&lt;/strong&gt;, R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeouts, retries, idempotency, degradation ladder&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A&lt;/strong&gt;, R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experiments, drills, burn-rate alarms&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A&lt;/strong&gt;, R&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;R = responsible, A = accountable, C = consulted, I = informed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS owns "resiliency of the cloud" and you own "resiliency in the cloud". Your share depends on the compute.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2.&lt;/strong&gt; You place instances across AZs, keep spare capacity and run the self-healing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers.&lt;/strong&gt; You spread tasks or pods across AZ subnets and own health checks. Fargate takes the hosts off your hands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda.&lt;/strong&gt; AWS runs functions across AZs. For VPC-attached functions, picking multi-AZ subnets is on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed AI.&lt;/strong&gt; The provider sets the lifecycle and AWS serves the model. Quotas, profiles, the ladder, EOL tracking and evals are yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Write one SLO for your most important journey and get the error budget policy signed.&lt;/li&gt;
&lt;li&gt;[ ] Multiply your hard dependencies. If the product is below your SLO, that gap is your backlog.&lt;/li&gt;
&lt;li&gt;[ ] Replace every recovery step that calls a control plane with pre-provisioned capacity or a health-check switch.&lt;/li&gt;
&lt;li&gt;[ ] Strip shared dependencies out of &lt;code&gt;/healthz&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Write down RTO and RPO per journey, then pick the DR strategy from the table.&lt;/li&gt;
&lt;li&gt;[ ] Run the DynamoDB endpoint-loss experiment in a sandbox account.&lt;/li&gt;
&lt;li&gt;[ ] Cut your busiest path down to one retry layer.&lt;/li&gt;
&lt;li&gt;[ ] List every production model ID with its EOL date.&lt;/li&gt;
&lt;li&gt;[ ] Decide, and test, what your AI feature shows when no model answers.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Availability is arithmetic before it's architecture. Multiply your dependencies to find your ceiling. Ask whether your copies fail independently to learn whether your redundancy is real. Ask whether recovery needs a control plane to learn whether it'll work on the day.&lt;/p&gt;

&lt;p&gt;AI doesn't change those rules. It adds a dependency with a quota, a lifecycle and a failure mode that returns HTTP 200. Put it in the math, give it a ladder, and make sure the last rung needs no model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nines are arithmetic. Recovery is rehearsal.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Availability&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Scalability (coming soon)&lt;/li&gt;
&lt;li&gt;Maintainability (coming soon)&lt;/li&gt;
&lt;li&gt;Cost (coming soon)&lt;/li&gt;
&lt;li&gt;Sustainability (coming soon)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up, Scalability.&lt;/strong&gt; A cost-benefit look at how you grow, where tokens per minute become a capacity unit as real as vCPUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/message/101925/" rel="noopener noreferrer"&gt;Summary of the Amazon DynamoDB Service Disruption in the Northern Virginia (US-EAST-1) Region — AWS, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ookla.com/articles/largest-outages-of-2025" rel="noopener noreferrer"&gt;Largest Outages of 2025: A Downdetector Analysis — Ookla, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.businesswire.com/news/home/20260513289344/en/Uptime-Announces-Annual-Outage-Analysis-Report-2026" rel="noopener noreferrer"&gt;Uptime Announces Annual Outage Analysis Report 2026 — Uptime Institute via Business Wire, May 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/workbook/implementing-slos/" rel="noopener noreferrer"&gt;Implementing SLOs — Google SRE Workbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/workbook/alerting-on-slos/" rel="noopener noreferrer"&gt;Alerting on SLOs — Google SRE Workbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/storage-classes/" rel="noopener noreferrer"&gt;Amazon S3 storage classes — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/availability.html" rel="noopener noreferrer"&gt;Availability — AWS Well-Architected Reliability Pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/whitepapers/latest/availability-and-beyond-improving-resilience/availability-with-dependencies.html" rel="noopener noreferrer"&gt;Availability with dependencies — AWS, Availability and Beyond whitepaper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/whitepapers/latest/availability-and-beyond-improving-resilience/availability-with-redundancy.html" rel="noopener noreferrer"&gt;Availability with redundancy — AWS, Availability and Beyond whitepaper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/api-gateway/sla/" rel="noopener noreferrer"&gt;Amazon API Gateway SLA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/lambda/sla/" rel="noopener noreferrer"&gt;AWS Lambda SLA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/dynamodb/sla/" rel="noopener noreferrer"&gt;Amazon DynamoDB SLA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/sla" rel="noopener noreferrer"&gt;Amazon Bedrock SLA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/route53/sla/" rel="noopener noreferrer"&gt;Amazon Route 53 SLA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reducing-scope-of-impact-with-cell-based-architecture/what-is-a-cell-based-architecture.html" rel="noopener noreferrer"&gt;What is a cell-based architecture? — AWS whitepaper, Reducing the Scope of Impact with Cell-Based Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://builder.aws.com/content/3K9SiqiJ369TGkkGzqPM95BuYWI/static-stability-using-availability-zones" rel="noopener noreferrer"&gt;Static stability using Availability Zones — Mike Furr, Amazon Builders' Library, 2019&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://builder.aws.com/content/3Ev53O39izHCtWLzp4XU6t8PC1O/implementing-health-checks" rel="noopener noreferrer"&gt;Implementing health checks — Amazon Builders' Library, 2019&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://builder.aws.com/content/3EuS9Sakq7L3VLQIF3qzfMfke1Y/avoiding-fallback-in-distributed-systems" rel="noopener noreferrer"&gt;Avoiding fallback in distributed systems — Jacob Gabrielson, Amazon Builders' Library, 2019&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html" rel="noopener noreferrer"&gt;Disaster recovery options in the cloud — AWS whitepaper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_planning_for_recovery_disaster_recovery.html" rel="noopener noreferrer"&gt;REL13-BP02 Use defined recovery strategies to meet the recovery objectives — AWS Well-Architected&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/resilience-hub/latest/userguide/concepts-terms.html" rel="noopener noreferrer"&gt;AWS Resilience Hub concepts and terms — Resilience Hub docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/shared-responsibility-model-for-resiliency.html" rel="noopener noreferrer"&gt;Shared Responsibility Model for Resiliency — AWS Well-Architected Reliability Pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/route-53-concepts.html" rel="noopener noreferrer"&gt;Route 53 control plane and data plane concepts — Route 53 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-determining-health-of-endpoints.html" rel="noopener noreferrer"&gt;How Route 53 determines whether a health check is healthy — Route 53 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-how-route-53-chooses-records.html" rel="noopener noreferrer"&gt;How Route 53 chooses records when health checking is configured — Route 53 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-types.html" rel="noopener noreferrer"&gt;Types of Route 53 health checks — Route 53 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/APIReference/API_HealthCheckConfig.html" rel="noopener noreferrer"&gt;HealthCheckConfig — Route 53 API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/route53/pricing/" rel="noopener noreferrer"&gt;Amazon Route 53 pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html" rel="noopener noreferrer"&gt;How Elastic Load Balancing works — ELB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/r53recovery/latest/dg/what-is-route53-recovery.html" rel="noopener noreferrer"&gt;What is Amazon Application Recovery Controller (ARC)? — ARC docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/r53recovery/latest/dg/arc-zonal-autoshift.html" rel="noopener noreferrer"&gt;Zonal autoshift in ARC — ARC docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/r53recovery/latest/dg/region-switch.html" rel="noopener noreferrer"&gt;Region switch in ARC — ARC docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/r53recovery/latest/dg/arc-readiness-availability-change.html" rel="noopener noreferrer"&gt;ARC readiness check availability change — ARC docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability" rel="noopener noreferrer"&gt;AWS Service Availability Updates — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database-disaster-recovery.html" rel="noopener noreferrer"&gt;Using switchover or failover in Amazon Aurora Global Database — Aurora docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2globaltables_HowItWorks.html" rel="noopener noreferrer"&gt;How DynamoDB global tables work — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/scenario-library-scenarios.html" rel="noopener noreferrer"&gt;AWS FIS scenario library — FIS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/fis-actions-reference.html" rel="noopener noreferrer"&gt;AWS FIS actions reference — FIS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/experiment-template-example.html" rel="noopener noreferrer"&gt;AWS FIS experiment template examples — FIS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/use-lambda-actions.html" rel="noopener noreferrer"&gt;Use the AWS FIS aws:lambda:function actions — FIS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cncf.io/projects/litmus/" rel="noopener noreferrer"&gt;LitmusChaos — CNCF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sloth.dev/" rel="noopener noreferrer"&gt;Sloth — Prometheus SLO generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/conquering-cloud-chaos-multi-account-experiments-with-aws-fis-injector-19ji"&gt;Conquering Cloud Chaos: Multi-Account Experiments with AWS FIS Injector — Oluwasegun Adedigba, dev.to, January 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_health_check" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_route53_health_check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record" rel="noopener noreferrer"&gt;Terraform AWS provider: aws_route53_record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/troubleshooting-api-error-codes.html" rel="noopener noreferrer"&gt;Troubleshooting Amazon Bedrock API error codes — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-token-burndown.html" rel="noopener noreferrer"&gt;How tokens are counted against quotas — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_mitigate_interaction_failure_limit_retries.html" rel="noopener noreferrer"&gt;REL05-BP03 Control and limit retry calls — AWS Well-Architected&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html" rel="noopener noreferrer"&gt;Retry behavior — AWS SDKs and Tools Reference Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-bedrock-reserved-service-tier" rel="noopener noreferrer"&gt;Amazon Bedrock introduces Reserved service tier — AWS, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html" rel="noopener noreferrer"&gt;Service tiers for model inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html" rel="noopener noreferrer"&gt;Increase throughput with cross-Region inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/optimize-your-applications-for-scale-and-reliability-on-amazon-bedrock/" rel="noopener noreferrer"&gt;Optimize your applications for scale and reliability on Amazon Bedrock — AWS Machine Learning Blog, February 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html" rel="noopener noreferrer"&gt;Converse — Amazon Bedrock API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-4-6.html" rel="noopener noreferrer"&gt;Claude Sonnet 4.6 model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html" rel="noopener noreferrer"&gt;Amazon Nova 2 Lite model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-4-5.html" rel="noopener noreferrer"&gt;Claude Sonnet 4.5 model card — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html" rel="noopener noreferrer"&gt;Model lifecycle — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GetFoundationModel.html" rel="noopener noreferrer"&gt;GetFoundationModel — Amazon Bedrock API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/choosing-workflow-type.html" rel="noopener noreferrer"&gt;Choosing workflow type in Step Functions — Step Functions docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;Lambda durable functions — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-lifecycle-settings.html" rel="noopener noreferrer"&gt;Configure Amazon Bedrock AgentCore lifecycle settings — AgentCore docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/replication.html" rel="noopener noreferrer"&gt;Cross-cluster replication for Amazon OpenSearch Service — OpenSearch Service docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-dynamodb-vector-search/" rel="noopener noreferrer"&gt;Amazon DynamoDB now supports real-time vector search — AWS, August 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/VectorSearchGlobalTables.html" rel="noopener noreferrer"&gt;Using vector indexes with global tables — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html" rel="noopener noreferrer"&gt;Amazon S3 Vectors — S3 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/genrel05-bp02.html" rel="noopener noreferrer"&gt;GENREL05-BP02 Replicate embedding data across all regions of availability — Well-Architected Generative AI Lens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.litellm.ai/docs/proxy/reliability" rel="noopener noreferrer"&gt;LiteLLM proxy reliability: retries, fallbacks, cooldowns — LiteLLM docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-devops-agent-generally-available" rel="noopener noreferrer"&gt;AWS DevOps Agent is now generally available — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/security-resilience.html" rel="noopener noreferrer"&gt;Resilience in AWS Lambda — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Accessibility</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:55:36 +0000</pubDate>
      <link>https://dev.to/cloudsege/non-functional-application-requirements-accessibility-15bm</link>
      <guid>https://dev.to/cloudsege/non-functional-application-requirements-accessibility-15bm</guid>
      <description>&lt;p&gt;On 4 June 2026, a court in Caen, France, ordered Carrefour to make its website and app fully accessible to disabled customers within six months, with a penalty for every day it runs late. Two disability associations, apiDV and Droit Pluriel, brought the case under the French law that carries the European Accessibility Act.&lt;/p&gt;

&lt;p&gt;Carrefour's defense was a number. It said the site already met 71% of the accessibility criteria. The judge said it had to be totally accessible (&lt;a href="https://www.deque.com/blog/frances-major-court-decision-supporting-digital-accessibility-under-the-eaa/" rel="noopener noreferrer"&gt;Deque, June 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Seventy-one percent sounds like a pass. For a blind shopper who can't get through checkout, it's a locked door.&lt;/p&gt;

&lt;p&gt;This part takes six popular myths about accessibility, puts each one against the evidence, and then builds the CI gate that stops you shipping your own 71%.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aim for &lt;strong&gt;WCAG 2.2 Level AA&lt;/strong&gt;. Meeting 2.2 also meets 2.0 and 2.1, which most laws still name.&lt;/li&gt;
&lt;li&gt;Six boring error types made up 96% of what WebAIM found in 2026. Native HTML prevents most of them for free.&lt;/li&gt;
&lt;li&gt;Gate CI on axe-core, add role-based Playwright tests, and keep a human screen reader pass, because a clickable &lt;code&gt;div&lt;/code&gt; sails through axe.&lt;/li&gt;
&lt;li&gt;The law is live: the EAA since 28 June 2025, the US ADA Title II rule from 26 April 2027.&lt;/li&gt;
&lt;li&gt;Streaming LLM answers should be announced in sentences, not tokens. The same markup serves screen readers and AI browser agents.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What: accessibility, defined precisely
&lt;/h2&gt;

&lt;p&gt;Accessibility means people with disabilities can perceive, understand, operate and navigate your product, with or without assistive technology like screen readers, magnifiers or voice control. Don't confuse it with &lt;strong&gt;conformance&lt;/strong&gt;, which is passing a standard's testable criteria. Conformance is the measurable proxy. Accessibility is the outcome, and you can have the first without the second.&lt;/p&gt;

&lt;p&gt;The measurable form is the &lt;strong&gt;Web Content Accessibility Guidelines (WCAG)&lt;/strong&gt;. Version 2.2 became a W3C Recommendation in 2023 and the international standard ISO/IEC 40500:2025 in October 2025. Its criteria come in levels A, AA and AAA, and when a law says "accessible", it almost always means &lt;strong&gt;AA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; web content, and in most legal readings apps and documents too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What 2.2 adds at A and AA:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2.4.11 Focus Not Obscured (AA):&lt;/strong&gt; sticky headers can't completely hide the focused element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.5.7 Dragging Movements (AA):&lt;/strong&gt; anything you drag also works with a single tap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.5.8 Target Size (AA):&lt;/strong&gt; pointer targets of at least 24 by 24 CSS pixels, or enough spacing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3.2.6 Consistent Help (A):&lt;/strong&gt; help links stay in the same place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3.3.7 Redundant Entry (A):&lt;/strong&gt; don't make people retype what they gave you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3.3.8 Accessible Authentication (AA):&lt;/strong&gt; no memory test or puzzle to sign in. Paste, password managers and passkeys satisfy it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A testable requirement reads: "Every route passes automated WCAG 2.2 AA checks on every pull request, sign-in and checkout complete with a keyboard and a screen reader, and no pointer target is under 24 by 24 CSS pixels."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: who you're leaving out, and who's watching
&lt;/h2&gt;

&lt;p&gt;About 1.3 billion people, 16% of the world's population, live with a significant disability (&lt;a href="https://www.who.int/news-room/fact-sheets/detail/disability-and-health" rel="noopener noreferrer"&gt;WHO, 2023&lt;/a&gt;). Add the temporary and situational cases: a broken wrist, glare on a phone, a supervisor on a loud factory floor who can't hear an alert.&lt;/p&gt;

&lt;p&gt;The web mostly fails them. WebAIM's 2026 scan of the top million home pages found detectable WCAG failures on 95.9% of them, up from 94.8% and reversing six years of slow improvement. The average page had 56.1 errors (&lt;a href="https://webaim.org/projects/million/" rel="noopener noreferrer"&gt;WebAIM Million, 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Then there's the law, which is where accessibility starts to look like &lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Part 3 on Compliance&lt;/a&gt;: a regulation, a standard it points to, and evidence you produce on demand. Myth 5 has the dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six myths, checked against the evidence
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6nvo2lhw7h55g7q7mvo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6nvo2lhw7h55g7q7mvo.png" alt="Six accessibility myths and what the evidence says. Accessibility is for a small group of users: one in six people has a disability, and captions and focus rings help everyone. It is expensive and slows us down: six common failures make up 96% of errors on home pages (WebAIM, 2026). A scanner or an overlay makes us compliant: ten free checkers together found 71% of 143 known barriers (GOV.UK, 2017). More ARIA makes it more accessible: pages using ARIA averaged 59.1 errors against 42 without (WebAIM, 2026). We are not in the EU, so the law cannot reach us: the European Accessibility Act has covered products and services sold to EU consumers since 28 June 2025. AI makes accessibility free: the US Justice Department says AI does not yet reliably fix inaccessible content at scale." width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 1: "Accessibility is for a small group of users"
&lt;/h3&gt;

&lt;p&gt;One in six people isn't a niche, and it undercounts your users. Captions get used in noisy rooms. A visible focus ring helps anyone fighting a bad trackpad.&lt;/p&gt;

&lt;p&gt;There's a newer group of users, too. Screen readers, voice control, your Playwright tests and AI browser agents all read the same thing: the &lt;strong&gt;accessibility tree&lt;/strong&gt; the browser builds from your markup. OpenAI's developer FAQ says "making your website more accessible helps ChatGPT Agent in Atlas understand it better". Microsoft's Playwright MCP server drives a browser for an LLM from that tree, not from screenshots.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  M["Your HTML&amp;lt;br/&amp;gt;and ARIA"] --&amp;gt; T["Browser accessibility tree&amp;lt;br/&amp;gt;roles, names, states"]
  T --&amp;gt; SR["Screen readers"]
  T --&amp;gt; VC["Voice control"]
  T --&amp;gt; PT["Playwright&amp;lt;br/&amp;gt;getByRole tests"]
  T --&amp;gt; AG["AI browser agents"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Every one of these reads the same tree, so one missing button name breaks all of them at once.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 2: "It's expensive and slows us down"
&lt;/h3&gt;

&lt;p&gt;Here are WebAIM's most common failures in 2026, by share of home pages: low contrast text (83.9%), missing alt text (53.1%), missing form labels (51%), empty links (46.3%), empty buttons (30.6%) and no page language (13.5%). Those six made up 96% of every error detected.&lt;/p&gt;

&lt;p&gt;Each one is a line of code. A checked color token, an &lt;code&gt;alt&lt;/code&gt; attribute, a &lt;code&gt;&amp;lt;label for&amp;gt;&lt;/code&gt;, text in a link, a name on an icon button, &lt;code&gt;lang="en"&lt;/code&gt;. None of that slows a sprint, and native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; bring keyboard support and the right role for free.&lt;/p&gt;

&lt;p&gt;The real cost lives in two places: retrofitting, which Carrefour now does on a judge's clock, and custom widgets like comboboxes and data grids. For those I'd use a tested library such as React Aria, which ships over 40 unstyled components tested with screen readers, rather than build my own. You still test what you build with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 3: "A scanner, or an overlay, makes us compliant"
&lt;/h3&gt;

&lt;p&gt;Automated tools are necessary and nowhere near enough. In 2017 the UK government built a page with 143 deliberate barriers and ran ten free checkers on it. Together they found 71%, and 42 barriers got past every tool (&lt;a href="https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/" rel="noopener noreferrer"&gt;GOV.UK, 2017&lt;/a&gt;). Deque, which makes axe, puts its own coverage at 57% of issues by volume (2021), a generous way of counting that still leaves four in ten for people.&lt;/p&gt;

&lt;p&gt;Here's what that looks like. A sign-in page with a &lt;code&gt;&amp;lt;div onclick&amp;gt;&lt;/code&gt; reading "Continue" passed axe-core 4.14 with zero violations. A Playwright test looking for a button named "Continue" failed immediately.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  D["div onclick&amp;lt;br/&amp;gt;with text Continue"] --&amp;gt; AX["axe-core scan"]
  AX --&amp;gt;|"no rule fails"| G["Green build"]
  D --&amp;gt; SR["Screen reader&amp;lt;br/&amp;gt;and keyboard"]
  SR --&amp;gt;|"read as plain text&amp;lt;br/&amp;gt;skipped by Tab"| X["User stuck&amp;lt;br/&amp;gt;at sign-in"]
  D --&amp;gt; RT["getByRole button test"]
  RT --&amp;gt;|"button not found"| R["Red build"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The same clickable div passes the scanner and fails both the user and a role-based test.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Overlays, the one-script-tag fixes, fail the same way at a higher price. In April 2025 the US Federal Trade Commission finalized a $1 million order against accessiBe, saying its AI-powered widget did not make all customer sites WCAG-compliant, and barred it from claiming otherwise without evidence (&lt;a href="https://www.ftc.gov/news-events/news/press-releases/2025/04/ftc-approves-final-order-requiring-accessibe-pay-1-million" rel="noopener noreferrer"&gt;FTC, 2025&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 4: "More ARIA makes it more accessible"
&lt;/h3&gt;

&lt;p&gt;In WebAIM's 2026 data, pages using ARIA averaged 59.1 errors against 42 without. WebAIM calls that a correlation, since ARIA-heavy pages are also more complex. It still matches the W3C's first rule of ARIA: if a native element already does what you need, use it.&lt;/p&gt;

&lt;p&gt;ARIA changes what assistive technology is told. It doesn't add behavior. &lt;code&gt;role="button"&lt;/code&gt; on a &lt;code&gt;div&lt;/code&gt; won't make it focusable or make Enter work.&lt;/p&gt;

&lt;p&gt;Agents add a twist. That OpenAI FAQ talks about "ARIA tags", and accessibility consultant Adrian Roselli replied that native HTML is the right advice and that ARIA labels will get keyword-stuffed like alt text was. Keep ARIA for what HTML can't say, like &lt;code&gt;aria-expanded&lt;/code&gt; or &lt;code&gt;aria-describedby&lt;/code&gt; wired up with React's &lt;code&gt;useId&lt;/code&gt;. And never write an &lt;code&gt;aria-label&lt;/code&gt; for a bot. WCAG 2.5.3 Label in Name expects the accessible name to contain the visible label, so voice control users can say what they see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Myth 5: "We're not in the EU or government, so the law can't reach us"
&lt;/h3&gt;

&lt;p&gt;The European Accessibility Act has applied since 28 June 2025 (&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882" rel="noopener noreferrer"&gt;EUR-Lex&lt;/a&gt;). It covers products and services for EU consumers, including e-commerce, banking, e-books, transport and telephony. Only microenterprises providing services (under 10 staff and €2 million turnover) are exempt, and each member state sets its own penalties. The standard behind it, EN 301 549, published version 4.1.1 with WCAG 2.2 AA in September 2026. At the time of writing it isn't yet cited in the EU's Official Journal, so version 3.2.1 is still the legal reference.&lt;/p&gt;

&lt;p&gt;In the US, the ADA Title II rule requires WCAG 2.1 AA for state and local government web content and apps, including what contractors supply. An April 2026 rule moved the dates to &lt;strong&gt;26 April 2027&lt;/strong&gt; for entities serving 50,000 people or more and &lt;strong&gt;26 April 2028&lt;/strong&gt; for the rest (&lt;a href="https://www.federalregister.gov/documents/2026/04/20/2026-07663/extension-of-compliance-dates-for-nondiscrimination-on-the-basis-of-disability-accessibility-of-web" rel="noopener noreferrer"&gt;Federal Register, 2026&lt;/a&gt;). The Justice Department says it may revisit the substance, so check before you plan around it.&lt;/p&gt;

&lt;p&gt;Enforcement is real but uneven. A month before Carrefour, a Lille court dismissed a similar case against Auchan's online arm on a revenue threshold, and that's under appeal. If you sell into Europe, or a city council buys your software, the law already reaches you.&lt;/p&gt;

&lt;h2&gt;
  
  
  When: where accessibility enters the lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design:&lt;/strong&gt; check contrast and focus styles in your tokens, keep targets at 24 by 24 CSS pixels, and annotate headings and button names in mockups. Developers can't guess what an icon was meant to say.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build:&lt;/strong&gt; native elements first, a tested library for complex widgets, &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; in the editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test:&lt;/strong&gt; axe-core plus role and keyboard tests on every pull request, then a screen reader pass (NVDA with Chrome, VoiceOver with Safari) on key journeys before each release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate:&lt;/strong&gt; publish an accessibility statement with a working feedback channel, treat accessibility bugs as defects, and re-check production journeys on a schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where: AWS building blocks and the open-source tools around them
&lt;/h2&gt;

&lt;p&gt;AWS doesn't sell an accessibility service. It gives you pieces that make particular criteria easier, and places to run checks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Amplify UI:&lt;/strong&gt; accessible React components, including the sign-in Authenticator. Useful on Amplify, unnecessary if you have a design system. Your theme can still break contrast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Cognito managed login:&lt;/strong&gt; passkeys and one-time codes from the Essentials tier up, which gives you a sign-in that meets 3.3.8. Test your branding's contrast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon CloudWatch Synthetics:&lt;/strong&gt; scheduled Playwright canaries (&lt;code&gt;syn-nodejs-playwright-8.0&lt;/code&gt;, Playwright 1.61.1) to replay journey tests against production. Its Playwright trails the current release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Transcribe:&lt;/strong&gt; live captions, with partial-result stabilization so words don't flicker. Recorded media still needs human-edited captions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Polly:&lt;/strong&gt; read-aloud at $4 to $100 per million characters by engine. Word-highlighting speech marks don't work with generative voices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core tools are open source. &lt;strong&gt;axe-core&lt;/strong&gt; powers most scanners, Lighthouse included, so Lighthouse isn't a second opinion. &lt;strong&gt;pa11y-ci&lt;/strong&gt; checks a URL list with no test code. And a free screen reader like NVDA is the only tool that tells you what a user will actually hear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: which check for which problem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;th&gt;Misses&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;eslint-plugin-jsx-a11y&lt;/td&gt;
&lt;td&gt;Click handlers on &lt;code&gt;div&lt;/code&gt;s, missing &lt;code&gt;alt&lt;/code&gt;, invalid ARIA&lt;/td&gt;
&lt;td&gt;Anything that depends on CSS or runtime state&lt;/td&gt;
&lt;td&gt;Free, seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;axe-core via Playwright&lt;/td&gt;
&lt;td&gt;Contrast, names, labels, &lt;code&gt;lang&lt;/code&gt;, invalid ARIA, target size with the 2.2 tag&lt;/td&gt;
&lt;td&gt;Fake controls, focus order, alt text quality&lt;/td&gt;
&lt;td&gt;Free, seconds per route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role and keyboard tests&lt;/td&gt;
&lt;td&gt;Fake controls, broken Tab order, mouse-only journeys&lt;/td&gt;
&lt;td&gt;Visual issues, meaning&lt;/td&gt;
&lt;td&gt;Free, you write them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch Synthetics canary&lt;/td&gt;
&lt;td&gt;Regressions in live journeys&lt;/td&gt;
&lt;td&gt;Whatever the script doesn't test&lt;/td&gt;
&lt;td&gt;Billed per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual screen reader pass&lt;/td&gt;
&lt;td&gt;Announcements, reading order, confusing flows&lt;/td&gt;
&lt;td&gt;Coverage at scale&lt;/td&gt;
&lt;td&gt;People's time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing with disabled users&lt;/td&gt;
&lt;td&gt;Whether it actually works for them&lt;/td&gt;
&lt;td&gt;Nothing a cheaper check finds better&lt;/td&gt;
&lt;td&gt;Recruiting and paying people&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you're not sure which check a requirement needs, walk it down this tree.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  Q["A WCAG requirement to verify"] --&amp;gt; Q1{"Visible in the DOM alone?&amp;lt;br/&amp;gt;contrast, names, labels, lang"}
  Q1 --&amp;gt;|"yes"| AX["axe-core in CI"]
  Q1 --&amp;gt;|"no"| Q2{"Depends on behavior?&amp;lt;br/&amp;gt;focus, keyboard, announcements"}
  Q2 --&amp;gt;|"yes"| PW["Playwright role&amp;lt;br/&amp;gt;and keyboard test"]
  Q2 --&amp;gt;|"no"| Q3{"Depends on meaning?&amp;lt;br/&amp;gt;alt text, captions, plain language"}
  Q3 --&amp;gt;|"yes"| HR["Human review&amp;lt;br/&amp;gt;AI may draft"]
  Q3 --&amp;gt;|"no"| AU
  AX --&amp;gt; AU["Before any conformance claim&amp;lt;br/&amp;gt;manual audit and user testing"]
  PW --&amp;gt; AU
  HR --&amp;gt; AU&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Pick the cheapest check that can see the problem, and never let a passing tool stand in for the audit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Accessibility also pulls with and against the rest of this series.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NFR&lt;/th&gt;
&lt;th&gt;Pulls the same way&lt;/th&gt;
&lt;th&gt;Pulls against it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Passkeys and password-manager paste meet 3.3.8 and cut phishing&lt;/td&gt;
&lt;td&gt;CAPTCHAs, and timeouts without the warning and 20-second extension WCAG 2.2.1 asks for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;The same evidence trail as Part 3&lt;/td&gt;
&lt;td&gt;Laws name different versions: 2.1 for ADA Title II, 2.2 in EN 301 549 v4.1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Native HTML ships less JavaScript than custom widgets or overlays&lt;/td&gt;
&lt;td&gt;Throttled announcements arrive after the text does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Announced error messages make degraded modes usable&lt;/td&gt;
&lt;td&gt;An accessible fallback is one more path to test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Text and voice share one backend&lt;/td&gt;
&lt;td&gt;Nova 2 Sonic caps at 20 concurrent sessions per account per Region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Fewer custom widgets, less keyboard code&lt;/td&gt;
&lt;td&gt;Hand-built ARIA widgets need their own tests forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;A lint error takes minutes to fix&lt;/td&gt;
&lt;td&gt;Manual testing and user research are real budget lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustainability&lt;/td&gt;
&lt;td&gt;Lean, semantic pages&lt;/td&gt;
&lt;td&gt;Speech output adds compute per interaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hands-on: a WCAG 2.2 AA gate in CI
&lt;/h2&gt;

&lt;p&gt;Take Harbor Works, the fictional plant in Luso, the open-source AI watchman for production lines I'm building alongside this series. Its web app has a status page, a sign-in page and an alerts page, and a shift supervisor has to be able to use each one with a keyboard and a screen reader.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  PR["Pull request"] --&amp;gt; L["ESLint jsx-a11y&amp;lt;br/&amp;gt;markup rules"]
  L --&amp;gt; B["Build the app"]
  B --&amp;gt; S["Playwright serves&amp;lt;br/&amp;gt;the production build"]
  S --&amp;gt; A["axe-core scan&amp;lt;br/&amp;gt;every route, 2.2 AA tags"]
  A --&amp;gt; J["Role and keyboard&amp;lt;br/&amp;gt;journey test"]
  J --&amp;gt;|"all pass"| M["Merge"]
  L --&amp;gt;|"fail"| F["Build fails with&amp;lt;br/&amp;gt;rule ids and selectors"]
  A --&amp;gt;|"fail"| F
  J --&amp;gt;|"fail"| F
  M --&amp;gt; H["Manual screen reader&amp;lt;br/&amp;gt;pass before release"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Each stage catches what the one before it can't, and a person still checks key journeys before release.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the lint step, add &lt;code&gt;jsxA11y.flatConfigs.recommended&lt;/code&gt; from &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; 6.10.2 to your &lt;code&gt;eslint.config.js&lt;/code&gt;. The Playwright config below assumes a Vite app with &lt;code&gt;npm run preview&lt;/code&gt;. Set &lt;code&gt;BASE_URL&lt;/code&gt; to test a preview deployment instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against &lt;a href="https://playwright.dev/docs/accessibility-testing" rel="noopener noreferrer"&gt;Playwright accessibility testing&lt;/a&gt; and &lt;a href="https://playwright.dev/docs/test-reporters" rel="noopener noreferrer"&gt;Playwright reporters&lt;/a&gt; on Oct 29, 2026. Ran locally with Node 22, @playwright/test 1.64.1, @axe-core/playwright 4.14.0 and Chromium against static test pages. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@playwright/test'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'./tests'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;forbidOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'list'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'never'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;// full report as a CI artifact&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'junit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reports/a11y-junit.xml'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// CodeBuild test reports read this&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:4173'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'chromium'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Desktop Chrome'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Serve the production build unless BASE_URL points at a preview deployment&lt;/span&gt;
  &lt;span class="n"&gt;webServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;A11Y_SERVE&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'npm run preview -- --port 4173 --strictPort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:4173'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reuseExistingServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test scans every route with axe, then walks sign-in the way a screen reader user would: by label, role and keyboard.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against &lt;a href="https://playwright.dev/docs/accessibility-testing" rel="noopener noreferrer"&gt;Playwright accessibility testing&lt;/a&gt; and &lt;a href="https://github.com/dequelabs/axe-core/blob/develop/doc/API.md" rel="noopener noreferrer"&gt;axe-core API tags&lt;/a&gt; on Oct 29, 2026. Ran locally as above. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/a11y.spec.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@playwright/test'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@axe-core/playwright'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// axe-core tags for WCAG 2.0, 2.1 and 2.2 at levels A and AA.&lt;/span&gt;
&lt;span class="c1"&gt;// Drop 'wcag22aa' and the WCAG 2.2 target-size rule never runs.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;WCAG_22_AA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wcag2a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wcag2aa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wcag21a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wcag21aa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wcag22aa'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;A11Y_ROUTES&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'/,/sign-in/,/alerts/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${route} has no detectable WCAG 2.2 AA violations`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// also fails pages with no main landmark&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WCAG_22_AA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[data-a11y-ignore]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;// third-party embeds you track as known issues&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Full JSON in the report: selectors, failure summaries and help URLs for whoever fixes it&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'axe-violations.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sb"&gt;`${v.id} [${v.impact}] x${v.nodes.length}: ${v.help}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// axe can't tell a clickable &amp;lt;div&amp;gt; from plain text. Role and label queries can:&lt;/span&gt;
&lt;span class="c1"&gt;// if this fails, screen readers, voice control and browser agents can't find the control either.&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sign-in works by label, role and keyboard alone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;A11Y_SIGN_IN&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'/sign-in/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'supervisor@harbor.example'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Tab'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'button'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Continue'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeFocused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// reachable, in order, by keyboard&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Enter'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'heading'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Alerts'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it showed three things. A broken test page failed with four named rules (&lt;code&gt;html-has-lang&lt;/code&gt;, &lt;code&gt;image-alt&lt;/code&gt;, &lt;code&gt;link-name&lt;/code&gt;, &lt;code&gt;color-contrast&lt;/code&gt;). The clickable-&lt;code&gt;div&lt;/code&gt; page passed the scan and failed the journey test. And axe-core 4.14 only checks target size when you ask for the &lt;code&gt;wcag22aa&lt;/code&gt; tag. A default run let 10-pixel buttons through. Keep &lt;code&gt;exclude&lt;/code&gt; rare, since it switches off every rule inside that element.&lt;/p&gt;

&lt;p&gt;On GitHub Actions:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against &lt;a href="https://playwright.dev/docs/ci-intro" rel="noopener noreferrer"&gt;Playwright CI docs&lt;/a&gt; on Oct 29, 2026. YAML validated, not run on GitHub. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/a11y.yml
&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a11y&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;a11y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx eslint .&lt;/span&gt;                       &lt;span class="c1"&gt;# jsx-a11y: div buttons, missing alt, before a browser starts
&lt;/span&gt;      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps chromium&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;                &lt;span class="c1"&gt;# GitHub sets CI=true, so test.only fails the build
&lt;/span&gt;      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ !cancelled() }}&lt;/span&gt;                 &lt;span class="c1"&gt;# upload the report when tests fail, too
&lt;/span&gt;        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a11y-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report/&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your pipeline runs in AWS, here's the same gate for CodeBuild. Playwright supports only Debian and Ubuntu, so use the Ubuntu standard image. CodeBuild doesn't document setting &lt;code&gt;CI&lt;/code&gt;, so the buildspec sets it. The JUnit file feeds CodeBuild test reports, so each failing page shows up as a failed test case.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html" rel="noopener noreferrer"&gt;CodeBuild buildspec reference&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html" rel="noopener noreferrer"&gt;CodeBuild available runtimes&lt;/a&gt; on Oct 29, 2026. YAML validated, not run in CodeBuild. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# buildspec.yml
# CodeBuild image: aws/codebuild/standard:7.0 (Ubuntu 22.04). Playwright supports Debian and Ubuntu only.
# Pinned alternative image: mcr.microsoft.com/playwright:v1.64.1-noble
&lt;/span&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;                 &lt;span class="c1"&gt;# the Playwright config reads it
&lt;/span&gt;
&lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runtime-versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nodejs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps chromium&lt;/span&gt;
  &lt;span class="na"&gt;pre_build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx eslint .&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;

&lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;a11y-tests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                  &lt;span class="c1"&gt;# becomes report group &amp;lt;project-name&amp;gt;-a11y-tests
&lt;/span&gt;    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;a11y-junit.xml&lt;/span&gt;
    &lt;span class="na"&gt;base-directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports&lt;/span&gt;
    &lt;span class="na"&gt;file-format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JUNITXML&lt;/span&gt;

&lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a11y-report&lt;/span&gt;
  &lt;span class="na"&gt;base-directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'**/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the automated slice, about half the problem by the most generous count. The manual pass in the diagram isn't optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility in the age of AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Myth 6: "AI makes accessibility free"
&lt;/h3&gt;

&lt;p&gt;The Justice Department answered this one. Delaying the ADA dates, it wrote that "advanced technology, such as generative AI, does not yet reliably automate the remediation of inaccessible content at scale". That's a regulator that wanted the shortcut to exist.&lt;/p&gt;

&lt;p&gt;Research agrees. A 2025 study comparing web code from GPT-4o and Qwen2.5-Coder with human code found the models often better on basics like contrast and alt text, and weak on ARIA. Better prompts helped a little. Feeding test results back to the model helped a lot. WebAIM even suggests AI-assisted "vibe coding" may be part of why errors rose in 2026, though it didn't measure that.&lt;/p&gt;

&lt;p&gt;Alt text shows why "free" is the wrong word. A model sees pixels, but alt text depends on purpose: the same photo needs empty alt as decoration and a description when it informs. Screen reader users can already get generic descriptions themselves (60.1% in WebAIM's 2026 screen reader survey use AI to describe images). What only you can add is why the image is there. Let AI draft alt text and captions, and have a person approve them.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as a workload: streaming answers and live regions
&lt;/h3&gt;

&lt;p&gt;Streaming makes chat feel fast and can make it unbearable with a screen reader. If the answer container is an &lt;code&gt;aria-live&lt;/code&gt; region, every token is a change, and users hear fragments, restarts, or a queue that runs long after the text has finished.&lt;/p&gt;

&lt;p&gt;The fix is to separate what's shown from what's announced. A Harbor Works supervisor asks why line 3 stopped. The visible answer updates per token. A separate &lt;code&gt;role="status"&lt;/code&gt; region, polite by default, only gets whole sentences, at most one every 1.5 seconds, then "Answer complete."&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant U as Supervisor
  participant UI as Chat UI
  participant B as Bedrock
  participant L as Live region
  participant SR as Screen reader
  U-&amp;gt;&amp;gt;UI: Why did line 3 stop
  UI-&amp;gt;&amp;gt;B: ConverseStream request
  loop every token
    B--&amp;gt;&amp;gt;UI: contentBlockDelta text
    UI-&amp;gt;&amp;gt;UI: Append to visible answer and sentence buffer
    opt sentence ended and 1.5 s since last announcement
      UI-&amp;gt;&amp;gt;L: Replace text with the whole sentence
      L-&amp;gt;&amp;gt;SR: Polite announcement
    end
  end
  B--&amp;gt;&amp;gt;UI: messageStop
  UI-&amp;gt;&amp;gt;L: Remaining text plus Answer complete
  L-&amp;gt;&amp;gt;SR: Final polite announcement&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Tokens reach the screen as fast as they arrive, but the screen reader only hears whole sentences.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your backend calls Bedrock's &lt;code&gt;ConverseStream&lt;/code&gt; and forwards each &lt;code&gt;contentBlockDelta&lt;/code&gt; text, and &lt;code&gt;getTokens&lt;/code&gt; reads that stream. Add a standard &lt;code&gt;visually-hidden&lt;/code&gt; CSS class.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions" rel="noopener noreferrer"&gt;MDN ARIA live regions&lt;/a&gt; and the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html" rel="noopener noreferrer"&gt;Bedrock ConverseStream reference&lt;/a&gt; on Oct 29, 2026. Type-checked with React 19 and run in Chromium against a simulated stream, not with a real screen reader or Bedrock. Not deployed — test in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'react'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A factory, not a stream: StrictMode runs effects twice, so each run needs its own request&lt;/span&gt;
  &lt;span class="n"&gt;getTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;AsyncIterable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;announce&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'sentences'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'complete'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// let users pick, and save it in their settings&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MIN_GAP_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// never speak more often than this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SENTENCE_END&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;!?&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;\&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;$&lt;span class="o"&gt;|&lt;/span&gt;\&lt;span class="n"&gt;n&lt;/span&gt;$&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;StreamingAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;getTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;announce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sentences'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setShown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// what sighted users see: every token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;spoken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setSpoken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// what the live region says: whole sentences&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lastSpoke&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setShown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;setSpoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;getTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nf"&gt;setShown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;announce&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'sentences'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;SENTENCE_END&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastSpoke&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="no"&gt;MIN_GAP_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setSpoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;lastSpoke&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// Flush the tail, then say it's finished so nobody sits waiting for more&lt;/span&gt;
        &lt;span class="nf"&gt;setSpoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;announce&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'complete'&lt;/span&gt;
          &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'Answer ready.'&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`${pending.current.trim()} Answer complete.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setSpoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The answer failed to load. Try again.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;getTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;announce&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="n"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;labelledby&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"answer-heading"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"answer-heading"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* not live: reading it is the user's choice */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* In the DOM from first render so screen readers register it before it changes */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt; &lt;span class="n"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"visually-hidden"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;spoken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a simulated run, a 48-token answer produced two announcements instead of 48 and passed the same axe scan. Some users would rather read the finished answer at their own pace, so make &lt;code&gt;announce="complete"&lt;/code&gt; a user setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voice interfaces
&lt;/h3&gt;

&lt;p&gt;Voice helps users with motor or vision impairments, and anyone whose hands are busy. On AWS you can chain &lt;strong&gt;Amazon Transcribe&lt;/strong&gt;, a text model and &lt;strong&gt;Amazon Polly&lt;/strong&gt;, keeping a transcript at every step. Or use &lt;strong&gt;Amazon Nova 2 Sonic&lt;/strong&gt; (&lt;code&gt;amazon.nova-2-sonic-v1:0&lt;/code&gt;, generally available since December 2025), a speech-to-speech model on Bedrock's &lt;code&gt;InvokeModelWithBidirectionalStream&lt;/code&gt; API that accepts speech and text in one session, in seven languages.&lt;/p&gt;

&lt;p&gt;Know its limits. It runs In-Region only (&lt;code&gt;us-east-1&lt;/code&gt;, &lt;code&gt;us-west-2&lt;/code&gt;, &lt;code&gt;eu-north-1&lt;/code&gt;, &lt;code&gt;ap-northeast-1&lt;/code&gt;), connections last eight minutes, and the quota of 20 concurrent sessions per account per Region isn't adjustable. Its model card lists Bedrock Guardrails as unsupported, so content controls live in your prompts and tools. And voice must never be the only way in. A Deaf user needs the same task in text.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI agents as users
&lt;/h3&gt;

&lt;p&gt;Browser agents read the accessibility tree, so Myths 1 and 4 apply to them as well. A form a screen reader can finish is a form an agent can finish, which makes your &lt;code&gt;getByRole&lt;/code&gt; tests a fair proxy for agent-readiness. If an agent can't find a button, the fix is the same native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; a person needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;p&gt;Assistants happily generate a &lt;code&gt;div&lt;/code&gt; with an &lt;code&gt;onClick&lt;/code&gt;, and that 2025 study suggests prompting alone won't fix it. Feedback does. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put "native elements, a label on every input, no click handlers on non-interactive elements" in your assistant's project instructions.&lt;/li&gt;
&lt;li&gt;Feed it the axe JSON and the failing role test, not "make it accessible".&lt;/li&gt;
&lt;li&gt;Have a person review every ARIA attribute it adds, since that's where models were weakest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's still immature is AI doing the manual pass. No tool today replaces a person with a screen reader on your checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who: owners and the shared responsibility split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Product owner&lt;/th&gt;
&lt;th&gt;Designer&lt;/th&gt;
&lt;th&gt;Developers&lt;/th&gt;
&lt;th&gt;QA / platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set the target and legal scope, with legal advice&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contrast, focus and target-size tokens&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic components, labels, live regions&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R/A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI gate and production canaries&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen reader pass before release&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approve AI-drafted alt text and captions&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;R = responsible, A = accountable, C = consulted, I = informed.&lt;/p&gt;

&lt;p&gt;Shared responsibility barely changes with compute type. On EC2, containers or Lambda, AWS owns its console's accessibility and nothing in your app. With Amplify UI, AWS owns component semantics, and you own labels, composition and theme. With Bedrock, the provider owns what the model generates, and you own how it's presented, announced, captioned and reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Write down your target (WCAG 2.2 AA) and which laws reach you: the EAA, ADA Title II through a public-sector customer, or both.&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; with the recommended config and fix what it finds in shared components.&lt;/li&gt;
&lt;li&gt;[ ] Add the axe-core Playwright test with the &lt;code&gt;wcag22aa&lt;/code&gt; tag to CI for your three busiest routes.&lt;/li&gt;
&lt;li&gt;[ ] Write one role-and-keyboard journey test for sign-in and one for your main task.&lt;/li&gt;
&lt;li&gt;[ ] Check design tokens for 4.5:1 text contrast and a visible focus style.&lt;/li&gt;
&lt;li&gt;[ ] Spend 15 minutes on checkout or sign-in with NVDA or VoiceOver, and file what you hear.&lt;/li&gt;
&lt;li&gt;[ ] If you stream LLM output, announce sentences from a separate &lt;code&gt;role="status"&lt;/code&gt; region.&lt;/li&gt;
&lt;li&gt;[ ] Put a human approval step on AI-generated alt text and captions.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Every myth here grows from one belief: that accessibility is a feature you add, buy or generate. The evidence says it's mostly a property of ordinary code. Six boring error types cover nearly every failure. A real &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; beats a clever widget. Scanners find part of the problem, overlays and AI drafts don't close the rest, and courts and regulators have stopped accepting "mostly".&lt;/p&gt;

&lt;p&gt;Carrefour's 71% measured the wrong thing. Your users don't experience a percentage. They finish the task or they don't.&lt;/p&gt;

&lt;p&gt;So build the gate, write the role tests, and put a person with a screen reader on the journeys that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If a screen reader can't find it, neither can your users, your tests or your agents.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Performance (coming soon)&lt;/li&gt;
&lt;li&gt;Availability (coming soon)&lt;/li&gt;
&lt;li&gt;Scalability (coming soon)&lt;/li&gt;
&lt;li&gt;Maintainability (coming soon)&lt;/li&gt;
&lt;li&gt;Cost (coming soon)&lt;/li&gt;
&lt;li&gt;Sustainability (coming soon)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up, Performance:&lt;/strong&gt; p95 budgets, caching layers and time-to-first-token, and why a page that's merely less slow still sells more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.deque.com/blog/frances-major-court-decision-supporting-digital-accessibility-under-the-eaa/" rel="noopener noreferrer"&gt;France's major court decision supporting digital accessibility under the EAA — Deque, June 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://informations.handicap.fr/a-accessibilite-web-carrefour-condamne-par-la-justice-39309.php" rel="noopener noreferrer"&gt;Accessibilité web : Carrefour condamné par la justice — Handicap.fr, June 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://silktide.com/blog/eaa-auchan-court-ruling/" rel="noopener noreferrer"&gt;EAA: Auchan court ruling — Silktide, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.who.int/news-room/fact-sheets/detail/disability-and-health" rel="noopener noreferrer"&gt;Disability fact sheet — World Health Organization, March 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webaim.org/projects/million/" rel="noopener noreferrer"&gt;The WebAIM Million: 2026 report on the accessibility of the top 1,000,000 home pages — WebAIM, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webaim.org/projects/screenreadersurvey11/" rel="noopener noreferrer"&gt;Screen Reader User Survey #11 results — WebAIM, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;Web Content Accessibility Guidelines (WCAG) 2.2 — W3C Recommendation, December 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/" rel="noopener noreferrer"&gt;What's new in WCAG 2.2 — W3C WAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/news/2025-10-21/wcag22-iso" rel="noopener noreferrer"&gt;WCAG 2.2 approved as an ISO standard — W3C WAI, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html" rel="noopener noreferrer"&gt;Understanding SC 2.5.8 Target Size (Minimum) — W3C WAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html" rel="noopener noreferrer"&gt;Understanding SC 3.3.8 Accessible Authentication (Minimum) — W3C WAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html" rel="noopener noreferrer"&gt;Understanding SC 2.2.1 Timing Adjustable — W3C WAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/using-aria/" rel="noopener noreferrer"&gt;Using ARIA: the first rule of ARIA use — W3C&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/tutorials/images/decision-tree/" rel="noopener noreferrer"&gt;An alt decision tree — W3C WAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882" rel="noopener noreferrer"&gt;Directive (EU) 2019/882, the European Accessibility Act — EUR-Lex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dwt.com/insights/2026/09/european-accessibility-act-ict-standards-update" rel="noopener noreferrer"&gt;European Accessibility Act ICT standards update — Davis Wright Tremaine, September 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.federalregister.gov/documents/2026/04/20/2026-07663/extension-of-compliance-dates-for-nondiscrimination-on-the-basis-of-disability-accessibility-of-web" rel="noopener noreferrer"&gt;Extension of compliance dates for the ADA Title II web and mobile accessibility rule — Federal Register, April 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ada.gov/title-ii-web-rule/" rel="noopener noreferrer"&gt;Title II web rule — ADA.gov&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ftc.gov/news-events/news/press-releases/2025/04/ftc-approves-final-order-requiring-accessibe-pay-1-million" rel="noopener noreferrer"&gt;FTC approves final order requiring accessiBe to pay $1 million — FTC, April 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/" rel="noopener noreferrer"&gt;What we found when we tested tools on the world's least-accessible webpage — GOV.UK Accessibility blog, 2017&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues" rel="noopener noreferrer"&gt;Automated testing study identifies 57 percent of digital accessibility issues — Deque, 2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.openai.com/en/articles/12627856-publishers-and-developers-faq" rel="noopener noreferrer"&gt;Publishers and developers FAQ — OpenAI Help Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adrianroselli.com/2025/10/openai-aria-and-seo-making-the-web-worse.html" rel="noopener noreferrer"&gt;OpenAI, ARIA, and SEO: Making the Web Worse — Adrian Roselli, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;Playwright MCP — Microsoft, GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2503.15885" rel="noopener noreferrer"&gt;Human or LLM? A Comparative Study on Accessible Code Generation Capability — Suh et al., arXiv, 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/accessibility-testing" rel="noopener noreferrer"&gt;Accessibility testing — Playwright docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/test-reporters" rel="noopener noreferrer"&gt;Reporters — Playwright docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/ci-intro" rel="noopener noreferrer"&gt;Setting up CI — Playwright docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;Installation and system requirements — Playwright docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/docker" rel="noopener noreferrer"&gt;Docker — Playwright docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dequelabs/axe-core/blob/develop/doc/API.md" rel="noopener noreferrer"&gt;axe-core API documentation — Deque, GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md" rel="noopener noreferrer"&gt;axe-core rule descriptions — Deque, GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jsx-eslint/eslint-plugin-jsx-a11y" rel="noopener noreferrer"&gt;eslint-plugin-jsx-a11y — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pa11y/pa11y" rel="noopener noreferrer"&gt;pa11y — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react-spectrum.adobe.com/react-aria/" rel="noopener noreferrer"&gt;React Aria — Adobe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react.dev/reference/react/useId" rel="noopener noreferrer"&gt;useId — React docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions" rel="noopener noreferrer"&gt;ARIA live regions — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/lighthouse/accessibility/scoring" rel="noopener noreferrer"&gt;Lighthouse accessibility scoring — Chrome for Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html" rel="noopener noreferrer"&gt;Buildspec reference — AWS CodeBuild docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html" rel="noopener noreferrer"&gt;Available runtimes — AWS CodeBuild docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ui.docs.amplify.aws/react/getting-started/accessibility" rel="noopener noreferrer"&gt;Accessibility — Amplify UI docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/improve-your-app-authentication-workflow-with-new-amazon-cognito-features/" rel="noopener noreferrer"&gt;Improve your app authentication workflow with new Amazon Cognito features — AWS News Blog, November 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_playwright.html" rel="noopener noreferrer"&gt;Playwright runtime versions for CloudWatch Synthetics — Amazon CloudWatch docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/transcribe/latest/dg/streaming.html" rel="noopener noreferrer"&gt;Streaming transcription — Amazon Transcribe docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/transcribe/latest/dg/streaming-partial-results.html" rel="noopener noreferrer"&gt;Partial-result stabilization — Amazon Transcribe docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/polly/latest/dg/speechmarks.html" rel="noopener noreferrer"&gt;Speech marks — Amazon Polly docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/polly/latest/dg/generative-voices.html" rel="noopener noreferrer"&gt;Generative voices — Amazon Polly docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/polly/pricing/" rel="noopener noreferrer"&gt;Amazon Polly pricing — AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-nova-2-sonic-real-time-conversational-ai" rel="noopener noreferrer"&gt;Announcing Amazon Nova 2 Sonic for real-time conversational AI — AWS, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-sonic.html" rel="noopener noreferrer"&gt;Nova 2 Sonic model card — Amazon Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/nova/latest/nova2-userguide/using-conversational-speech.html" rel="noopener noreferrer"&gt;Speech-to-speech (Amazon Nova 2 Sonic) — Amazon Nova docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/nova/latest/nova2-userguide/release-notes.html" rel="noopener noreferrer"&gt;Amazon Nova 2 release notes — Amazon Nova docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModelWithBidirectionalStream.html" rel="noopener noreferrer"&gt;InvokeModelWithBidirectionalStream — Amazon Bedrock API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html" rel="noopener noreferrer"&gt;ConverseStream — Amazon Bedrock API reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Performance</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 04 Nov 2026 09:42:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-performance-49f9</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-performance-49f9</guid>
      <description>&lt;p&gt;In 2021, Vodafone published a test every team arguing about performance should know. Two landing pages looked and worked the same, with paid traffic split 50/50, about 34,000 visits a day each. One version rendered its critical HTML on the server, moved a widget off the client and resized its hero image. Its Largest Contentful Paint was 31% better in field data (5.7 s against 8.3 s).&lt;/p&gt;

&lt;p&gt;That page sold 8% more, with 15% more leads and 11% more carts per visit (&lt;a href="https://web.dev/case-studies/vodafone" rel="noopener noreferrer"&gt;web.dev&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Here's the part I like: 5.7 seconds is still "poor" by Google's thresholds (over 4). The winning page wasn't fast, just less slow, and people still bought more.&lt;/p&gt;

&lt;p&gt;So when someone says performance can wait until after launch, show them this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Budget on &lt;strong&gt;percentiles&lt;/strong&gt; (p95/p99), and say whether you mean latency or throughput. Averages hide the users you lose.&lt;/li&gt;
&lt;li&gt;Cache in layers (edge, application, database), picked by how fresh the data must be.&lt;/li&gt;
&lt;li&gt;Lambda cold starts hit under 1% of invocations (AWS), which is exactly your tail.&lt;/li&gt;
&lt;li&gt;Load-test with an &lt;strong&gt;open model&lt;/strong&gt; and fail the build when p95 goes over budget.&lt;/li&gt;
&lt;li&gt;For LLM features, budget &lt;strong&gt;time-to-first-token&lt;/strong&gt;, &lt;strong&gt;output tokens/sec&lt;/strong&gt; and &lt;strong&gt;end-to-end p95&lt;/strong&gt; separately. Streaming and prompt caching are your biggest levers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What performance actually means
&lt;/h2&gt;

&lt;p&gt;Performance is how fast, and how much, work your system does for a user under a stated load. Two numbers make it testable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency&lt;/strong&gt; is the time one request takes between a defined start and end, so latency at the load balancer and in the browser are different numbers. &lt;strong&gt;Throughput&lt;/strong&gt; is work completed per unit of time (requests, bytes, tokens) at an acceptable latency. Without a latency target it's meaningless, since any queue accepts work if nobody cares when it finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Percentiles&lt;/strong&gt; describe latency's shape: p50 is the median, p95 the value 95% of requests beat, p99 the slowest 1%. Averages describe no real user: if 98 requests take 50 ms and 2 take 3 seconds, the average is 109 ms and the p99 is 3 seconds.&lt;/p&gt;

&lt;p&gt;That high end is &lt;strong&gt;tail latency&lt;/strong&gt;, and it compounds. If one server is slow on 1 request in 100, a request fanning out to 100 such servers is slow &lt;strong&gt;63%&lt;/strong&gt; of the time (&lt;a href="https://cacm.acm.org/research/the-tail-at-scale/" rel="noopener noreferrer"&gt;The Tail at Scale&lt;/a&gt;). Microservices, RAG pipelines and agent tool calls all fan out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfk8kus6jvxh4qdeyq59.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfk8kus6jvxh4qdeyq59.png" alt="Why averages lie: averages hide the slow users. When 98 requests take 50 ms and 2 take 3 seconds, the average is 109 ms but the p99 is 3 seconds. Tail latency compounds: a request touching 100 servers that are each slow 1 time in 100 is slow 63% of the time." width="800" height="801"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For frontends, Google's &lt;strong&gt;Core Web Vitals&lt;/strong&gt; are judged at p75 of page loads. &lt;strong&gt;LCP&lt;/strong&gt; (loading) is good at 2.5 s or less and poor over 4.0 s. &lt;strong&gt;INP&lt;/strong&gt; (responsiveness, which replaced FID in 2024) is good at 200 ms or less and poor over 500 ms. &lt;strong&gt;CLS&lt;/strong&gt; (visual stability) is good at 0.1 or less and poor over 0.25.&lt;/p&gt;

&lt;p&gt;A testable requirement reads: "Product page LCP ≤ 2.5 s at p75 on mobile. &lt;code&gt;GET /products/{id}&lt;/code&gt; p95 ≤ 150 ms and p99 ≤ 400 ms at 200 requests/second."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's worth the effort
&lt;/h2&gt;

&lt;p&gt;It isn't a one-off. A Google-commissioned Deloitte and 55 study of 37 European and US brands and more than 30 million sessions found a 0.1-second improvement in mobile site speed lifted retail conversions by &lt;strong&gt;8.4%&lt;/strong&gt; and travel conversions by &lt;strong&gt;10.1%&lt;/strong&gt; (&lt;a href="https://web.dev/case-studies/milliseconds-make-millions" rel="noopener noreferrer"&gt;Milliseconds Make Millions, 2020&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Distance matters too. AWS's first CloudFront edge location in Lagos (June 2023) brought up to a 20% average latency improvement for content it served. If your users are in West Africa and your origin is in &lt;code&gt;eu-west-1&lt;/code&gt;, physics is part of your budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to think about it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design:&lt;/strong&gt; write the budget per user journey and split it across hops. Of the page's 2.5 seconds, how much goes to the API, and how much to the database?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build:&lt;/strong&gt; emit latency with a cache hit or miss dimension, and profile before you optimize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test:&lt;/strong&gt; load-test at the target arrival rate before every release, on production-shaped data, with thresholds that fail the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate:&lt;/strong&gt; alarm on p95 and p99, not the average, and watch CloudWatch RUM beside synthetic tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the time goes, and what to use
&lt;/h2&gt;

&lt;p&gt;Most performance mistakes are the right tool on the wrong job. First, the layers.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    U["User"] --&amp;gt;|"TLS ends at a nearby edge"| CF["CloudFront&amp;lt;br/&amp;gt;static assets and cacheable GETs"]
    CF --&amp;gt;|"miss or dynamic request"| AG["API Gateway"]
    AG --&amp;gt; L["Lambda&amp;lt;br/&amp;gt;connections reused when warm"]
    L --&amp;gt;|"check cache first"| V["ElastiCache for Valkey"]
    V --&amp;gt;|"hit: return"| L
    L --&amp;gt;|"on miss: GetItem"| D["DynamoDB"]
    D --&amp;gt; L
    L --&amp;gt;|"SET with jittered TTL"| V&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Each hop to the right adds latency and load, and each cache exists to stop the request going further.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge caching with CloudFront
&lt;/h3&gt;

&lt;p&gt;CloudFront's speed comes from its hit ratio, which comes from the cache key. Every header, cookie or query string in it splits the cache. Two cookies with three values each can mean nine origin requests per object.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    R["Requests for one object"] --&amp;gt; T["Tight cache key&amp;lt;br/&amp;gt;only what the origin uses"]
    R --&amp;gt; W["Cache key with two cookies&amp;lt;br/&amp;gt;three values each"]
    T --&amp;gt; T1["1 cached copy&amp;lt;br/&amp;gt;1 origin request"]
    W --&amp;gt; W1["3 x 3 = 9 cached copies&amp;lt;br/&amp;gt;9 origin requests"]
    T1 --&amp;gt; H["Higher hit ratio"]
    W1 --&amp;gt; M["Lower hit ratio"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Every value you add to the cache key multiplies the copies CloudFront has to fetch from your origin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pick the policy by content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CachingOptimized&lt;/code&gt;&lt;/strong&gt; (24-hour default TTL) suits versioned static assets. Keep it off changing HTML and API responses, because its 1-second minimum TTL overrides an origin's &lt;code&gt;no-cache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UseOriginCacheControlHeaders&lt;/code&gt;&lt;/strong&gt; works when your origin sends correct &lt;code&gt;Cache-Control&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CachingDisabled&lt;/code&gt;&lt;/strong&gt; is for personalized or write paths, not public content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin Shield&lt;/strong&gt; helps with global viewers and expensive origins. It adds a per-request charge, so skip it for dynamic or rarely requested content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For edge logic, CloudFront Functions do sub-millisecond cache-key and header work, with no network access and 10 KB of code. Lambda@Edge gets network access and up to 30 seconds, but caps at 10,000 requests per second per Region.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;stale-while-revalidate&lt;/code&gt; and &lt;code&gt;stale-if-error&lt;/code&gt; to &lt;code&gt;Cache-Control&lt;/code&gt; so users get a cached answer while CloudFront refreshes. The same goes for Cloudflare or Fastly. The key matters more than the CDN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute: Lambda cold starts
&lt;/h3&gt;

&lt;p&gt;AWS says cold starts "typically occur in under 1% of invocations" in production (&lt;a href="https://aws.amazon.com/blogs/compute/aws-lambda-standardizes-billing-for-init-phase/" rel="noopener noreferrer"&gt;AWS Compute Blog, 2025&lt;/a&gt;). That 1% is your p99. Since 1 August 2025 INIT is billed in all configurations, so slow init costs money.&lt;/p&gt;

&lt;p&gt;Your four options solve different problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Right-size memory first.&lt;/strong&gt; CPU scales with memory, and 1,769 MB is one vCPU. AWS Lambda Power Tuning (open source) finds the sweet spot, and shorter duration often offsets the higher GB-second price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SnapStart&lt;/strong&gt; restores a snapshot of an initialized environment, for Java 11+, Python 3.12+ and .NET 8+ with heavy init. It can't be used with provisioned concurrency, EFS and ephemeral storage over 512 MB. It's free for Java, while Python and .NET pay cache and restore charges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provisioned concurrency&lt;/strong&gt; keeps N environments initialized, for strict latency on spiky but predictable traffic. It bills continuously, used or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda Managed Instances&lt;/strong&gt; run Lambda on EC2 in your account, many invocations per environment, no cold starts. They fit steady, high-volume traffic at the EC2 price plus a 15% management fee, and code must be thread-safe.
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    A["Is p99 dominated by Init Duration?"] --&amp;gt;|"no"| Z["Fix the hot path first:&amp;lt;br/&amp;gt;memory size, client reuse, slow downstreams"]
    A --&amp;gt;|"yes"| B["Java 11+, Python 3.12+ or .NET 8+?"]
    B --&amp;gt;|"yes"| C["Try SnapStart&amp;lt;br/&amp;gt;check EFS and storage limits"]
    B --&amp;gt;|"no"| D["What does traffic look like?"]
    C --&amp;gt;|"still over budget"| D
    D --&amp;gt;|"steady and high volume"| E["Lambda Managed Instances&amp;lt;br/&amp;gt;or Fargate"]
    D --&amp;gt;|"spiky with known peaks"| F["Provisioned concurrency&amp;lt;br/&amp;gt;with scheduled scaling"]
    D --&amp;gt;|"spiky and rare"| G["Accept cold starts&amp;lt;br/&amp;gt;trim package and init code"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;The first question is a measurement, because many "cold start problems" are slow code running during INIT.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On an Application Load Balancer with uneven request cost, set the target group to &lt;code&gt;least_outstanding_requests&lt;/code&gt;, since round robin queues behind busy targets. You lose slow start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data: DynamoDB vs DAX vs ElastiCache
&lt;/h3&gt;

&lt;p&gt;Part 1 promised this comparison. ElastiCache runs &lt;strong&gt;Valkey&lt;/strong&gt; (the open-source Linux Foundation fork of Redis), &lt;strong&gt;Redis OSS&lt;/strong&gt; or &lt;strong&gt;Memcached&lt;/strong&gt;, serverless or node-based.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DynamoDB alone&lt;/th&gt;
&lt;th&gt;DynamoDB + DAX&lt;/th&gt;
&lt;th&gt;ElastiCache for Valkey&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typical read latency&lt;/td&gt;
&lt;td&gt;Single-digit milliseconds&lt;/td&gt;
&lt;td&gt;Microseconds for cached eventually consistent reads&lt;/td&gt;
&lt;td&gt;In-memory. Measure your own p99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code change&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Swap in the DAX client&lt;/td&gt;
&lt;td&gt;You write the cache logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;Strong or eventual, per read&lt;/td&gt;
&lt;td&gt;Eventual only. Strongly consistent reads pass through uncached&lt;/td&gt;
&lt;td&gt;Whatever you build: TTLs, invalidation, write-through&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staleness trap&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Query/Scan cache is not invalidated by item writes, and writes made directly to DynamoDB bypass DAX&lt;/td&gt;
&lt;td&gt;Forgotten invalidation. Stampedes when hot keys expire together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Choose when&lt;/td&gt;
&lt;td&gt;Latency already meets budget&lt;/td&gt;
&lt;td&gt;Read-heavy DynamoDB with hit rate above 90%&lt;/td&gt;
&lt;td&gt;Multiple data sources, computed results, or semantic caching for AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watch out&lt;/td&gt;
&lt;td&gt;Hot partitions&lt;/td&gt;
&lt;td&gt;VPC-only. Unbounded attribute names exhaust memory&lt;/td&gt;
&lt;td&gt;Serverless caches need TLS. Vector search needs node-based Valkey 8.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhrgzfqqckymkvre50eoy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhrgzfqqckymkvre50eoy.png" alt="AWS data read options compared. DynamoDB: single-digit millisecond reads, strong or eventual consistency, no extra code. DAX: microsecond reads, drop-in client, eventually consistent, query cache not cleared by writes. ElastiCache for Valkey: fast, caches anything, but you write the invalidation logic." width="800" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The staleness row catches teams out, so read the &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.consistency.html" rel="noopener noreferrer"&gt;DAX consistency docs&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Start with DynamoDB alone and measure. Before you add a cache, I'd check the access pattern (a scan, a hot partition key), because fixing key design is cheaper than running a cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load testing tools
&lt;/h3&gt;

&lt;p&gt;The load model matters more than the tool. &lt;strong&gt;k6&lt;/strong&gt; (open source) gives you an open model via &lt;code&gt;constant-arrival-rate&lt;/code&gt;, with thresholds that turn p95 budgets into CI pass or fail, though big tests need distributed runners. &lt;strong&gt;Locust&lt;/strong&gt; (open source) suits Python teams and complex flows, with scripts in Python only. &lt;strong&gt;Apache JMeter&lt;/strong&gt; (GUI and XML plans) covers the most protocols but is heavy to maintain as code. &lt;strong&gt;Distributed Load Testing on AWS&lt;/strong&gt; (v4.3.2) runs JMeter, k6 or Locust on Fargate across Regions, though you pay for the stack and still design the test.&lt;/p&gt;

&lt;p&gt;Use an &lt;strong&gt;open model&lt;/strong&gt;. In a closed model each virtual user waits for its last response, so a slowing system receives &lt;em&gt;less&lt;/em&gt; load and the test hides the problem. That's coordinated omission (&lt;a href="https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/" rel="noopener noreferrer"&gt;k6 docs&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide: the right fix, and what it costs elsewhere
&lt;/h2&gt;

&lt;p&gt;Match the symptom to the fix before buying capacity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your problem is…&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;th&gt;Not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static assets slow far from the origin&lt;/td&gt;
&lt;td&gt;CloudFront with a tight cache key&lt;/td&gt;
&lt;td&gt;A bigger origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeated identical reads on DynamoDB&lt;/td&gt;
&lt;td&gt;DAX&lt;/td&gt;
&lt;td&gt;A hand-written cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expensive computed results or mixed sources&lt;/td&gt;
&lt;td&gt;ElastiCache for Valkey, cache-aside&lt;/td&gt;
&lt;td&gt;DAX (it caches items, not your computations)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Init Duration in your p99&lt;/td&gt;
&lt;td&gt;SnapStart, then provisioned concurrency&lt;/td&gt;
&lt;td&gt;More memory alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady high-volume compute&lt;/td&gt;
&lt;td&gt;Lambda Managed Instances or Fargate&lt;/td&gt;
&lt;td&gt;Provisioned concurrency sized for peak all day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM responses feel slow&lt;/td&gt;
&lt;td&gt;Streaming, prompt caching, smaller model&lt;/td&gt;
&lt;td&gt;A bigger instance of anything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each fix touches the other NFRs. Some effects help. Each cache hit is load the database never sees (&lt;strong&gt;scalability&lt;/strong&gt;), &lt;code&gt;stale-if-error&lt;/code&gt; serves cached content while the origin is down (&lt;strong&gt;availability&lt;/strong&gt;), fast INP helps low-end devices (&lt;strong&gt;accessibility&lt;/strong&gt;), and cache hits and smaller models cost less (&lt;strong&gt;cost&lt;/strong&gt;) and use less compute per request (&lt;strong&gt;sustainability&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Others cost you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; a cache key missing the user ID serves one user's page to another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance:&lt;/strong&gt; latency-optimized and cross-Region inference process prompts outside your home Region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility:&lt;/strong&gt; streaming text floods screen readers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability:&lt;/strong&gt; a cold cache after failover stampedes the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; a cache sized for today's keys evicts hot ones at tomorrow's volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability:&lt;/strong&gt; invalidation logic is a classic source of bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; provisioned concurrency, the Priority tier and idle nodes bill regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustainability:&lt;/strong&gt; always-warm capacity burns energy at idle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that means skip the work. Just write down what you traded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on: cache-aside in front of DynamoDB, with a p95 gate
&lt;/h2&gt;

&lt;p&gt;Let's build the middle of the first diagram: a Lambda read path for &lt;code&gt;GET /products/{id}&lt;/code&gt; with ElastiCache Serverless for Valkey in front of DynamoDB, plus a k6 gate. The cache can speed requests up but never break them, TTLs carry jitter so hot keys don't expire together, and latency is emitted per path.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant G as API Gateway
    participant L as Lambda
    participant V as Valkey
    participant D as DynamoDB
    participant CW as CloudWatch
    G-&amp;gt;&amp;gt;L: GET /products/id
    L-&amp;gt;&amp;gt;V: GET product key, 50 ms timeout
    alt cache hit
        V--&amp;gt;&amp;gt;L: cached product
    else miss, timeout or cache error
        L-&amp;gt;&amp;gt;D: GetItem, eventually consistent
        D--&amp;gt;&amp;gt;L: item
        L-&amp;gt;&amp;gt;V: SET with TTL of 300 s, plus or minus 10%
        Note over L,V: a failed SET is ignored
    end
    L-&amp;gt;&amp;gt;CW: EMF log line, ProductReadLatency by CacheResult
    L--&amp;gt;&amp;gt;G: 200 with the product, or 404&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Every cache failure falls through to DynamoDB, and the jittered TTL spreads out when hot keys expire.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the &lt;a href="https://valkey-py.readthedocs.io/en/latest/connections.html" rel="noopener noreferrer"&gt;valkey-py docs&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/boto3/latest/reference/services/dynamodb/client/get_item.html" rel="noopener noreferrer"&gt;boto3 DynamoDB get_item&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/in-transit-encryption.html" rel="noopener noreferrer"&gt;ElastiCache in-transit encryption&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html" rel="noopener noreferrer"&gt;CloudWatch Embedded Metric Format&lt;/a&gt; on Oct 28, 2026. Not deployed, so try it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# handler.py — cache-aside read path. Python 3.12 Lambda in the cache's VPC,&lt;/span&gt;
&lt;span class="c1"&gt;# with a DynamoDB gateway VPC endpoint so it can reach the table without NAT.&lt;/span&gt;
&lt;span class="c1"&gt;# Package dependency: valkey (pip install valkey). boto3 is in the Lambda runtime.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;TypeDeserializer&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;valkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;ValkeyCluster&lt;/span&gt;

&lt;span class="no"&gt;TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TABLE_NAME"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CACHE_TTL_SECONDS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"300"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Created once per execution environment, outside the handler, so warm invocations reuse them.&lt;/span&gt;
&lt;span class="n"&gt;ddb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dynamodb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;deser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TypeDeserializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Lazy: if the cache is unreachable at cold start, the function still serves from DynamoDB.&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ValkeyCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CACHE_ENDPOINT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# serverless cache endpoint&lt;/span&gt;
            &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# serverless caches have in-transit encryption enabled&lt;/span&gt;
            &lt;span class="n"&gt;socket_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 50 ms: a slow cache must not be slower than the database&lt;/span&gt;
            &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;emit_latency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Embedded Metric Format: this log line becomes a CloudWatch metric, no PutMetricData call.&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"_aws"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"CloudWatchMetrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;"Namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Nfr/Performance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CacheResult"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ProductReadLatency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Milliseconds"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"CacheResult"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cache_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"ProductReadLatency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"product:{product_id}"&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"hit"&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# cache trouble degrades to a database read; it never fails the request&lt;/span&gt;
    &lt;span class="c1"&gt;# Eventually consistent read (the default): half the cost of a strongly consistent one.&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"pk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"S"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Item"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"miss"&lt;/span&gt;  &lt;span class="c1"&gt;# not cached: consider caching misses briefly if bots probe random IDs&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# +/-10% jitter spreads expiry&lt;/span&gt;
        &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"miss"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"pathParameters"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;emit_latency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Graph &lt;code&gt;ProductReadLatency&lt;/code&gt; p99 by &lt;code&gt;CacheResult&lt;/code&gt;. If &lt;code&gt;hit&lt;/code&gt; and &lt;code&gt;miss&lt;/code&gt; sit close, the cache isn't helping. Delete it.&lt;/p&gt;

&lt;p&gt;Now the gate. This k6 test holds 200 requests per second for ten minutes and exits non-zero if p95 or p99 breaks budget.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against the k6 &lt;a href="https://grafana.com/docs/k6/latest/using-k6/thresholds/" rel="noopener noreferrer"&gt;thresholds&lt;/a&gt; and &lt;a href="https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate/" rel="noopener noreferrer"&gt;constant-arrival-rate&lt;/a&gt; docs on Oct 28, 2026. Not deployed, so try it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// load-test.js — run: k6 run -e BASE_URL=https://your-api.example.com load-test.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'k6/http'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'k6'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;steady_reads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'constant-arrival-rate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// open model: load does not drop when the API slows&lt;/span&gt;
      &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// 200 iterations...&lt;/span&gt;
      &lt;span class="n"&gt;timeUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'1s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// ...per second&lt;/span&gt;
      &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'10m'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;preAllocatedVUs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;maxVUs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// headroom to hold the rate as latency rises&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'http_req_duration{endpoint:product}'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'p(95)&amp;lt;150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'p(99)&amp;lt;400'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;http_req_failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'rate&amp;lt;0.01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abortOnFail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delayAbortEval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'30s'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rate&amp;gt;0.99'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'p-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'p-002'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'p-003'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'p-004'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'p-005'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Skewed traffic: 80% of reads hit one hot item, as in most real catalogs.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${__ENV.BASE_URL}/products/${id}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'status is 200'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a real run, load thousands of IDs with &lt;code&gt;SharedArray&lt;/code&gt; so the hit ratio looks like production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance in the age of AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LLM features need three budgets
&lt;/h3&gt;

&lt;p&gt;An LLM call spends most of its time generating output, but users judge it by when the first words appear. Give it three budgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to first token (TTFT)&lt;/strong&gt; covers queueing and prompt processing. Prompt length and caching, model and tier move it, and Bedrock added a &lt;code&gt;TimeToFirstToken&lt;/code&gt; CloudWatch metric for streaming APIs in March 2026. &lt;strong&gt;Output tokens per second (OTPS)&lt;/strong&gt; is generation speed, set by the model, the tier and latency-optimized inference. You derive it as &lt;code&gt;OutputTokenCount / (InvocationLatency − TimeToFirstToken)&lt;/code&gt;. &lt;strong&gt;End-to-end p95&lt;/strong&gt; is everything, retrieval and tools included. &lt;code&gt;maxTokens&lt;/code&gt;, agent steps and retrieval move it, and your trace spans show it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexkbb2oemg209opguvwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexkbb2oemg209opguvwj.png" alt="Three latency budgets for an LLM request. Time to first token covers cold start, retrieval, queueing and prompt processing. Output tokens per second is generation speed, moved by model choice and the Priority tier. End-to-end p95 includes everything, including agent tool calls and output length." width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant U as Browser
    participant G as API Gateway
    participant L as Lambda
    participant R as Retriever
    participant B as Bedrock
    U-&amp;gt;&amp;gt;G: POST /chat
    G-&amp;gt;&amp;gt;L: invoke in STREAM mode
    Note over L: cold start only if no warm environment
    L-&amp;gt;&amp;gt;R: embed question and vector search
    R--&amp;gt;&amp;gt;L: top-k chunks
    L-&amp;gt;&amp;gt;B: ConverseStream with cached prefix
    Note over B: queue and prefill, a cache hit shortens this
    B--&amp;gt;&amp;gt;L: first token
    L--&amp;gt;&amp;gt;U: first chunk, TTFT ends here
    loop decode at OTPS
        B--&amp;gt;&amp;gt;L: tokens
        L--&amp;gt;&amp;gt;U: chunks
    end
    B--&amp;gt;&amp;gt;L: metadata with usage and latencyMs
    L--&amp;gt;&amp;gt;U: stream closes, end-to-end ends here&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Everything before the first chunk (cold start, retrieval, prefill) is dead air for the user, so optimize there first.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming, and the plumbing that limits it
&lt;/h3&gt;

&lt;p&gt;Streaming is the biggest perceived-performance lever, but the plumbing has limits. API Gateway REST integrations time out at 29 seconds. Regional and private APIs can raise it, possibly lowering your throttle quota. HTTP APIs stop at a fixed 30 seconds. Since November 2025, REST APIs can stream proxy integrations for up to 15 minutes (5-minute idle timeout on Regional endpoints), without endpoint caching or VTL.&lt;/p&gt;

&lt;p&gt;Lambda streams natively on Node.js, while Python needs a custom runtime or the Lambda Web Adapter. Streams reach 200 MB, capped at 2 MBps after the first 6 MB.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bedrock levers
&lt;/h3&gt;

&lt;p&gt;Once you stream, these are Bedrock's knobs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;th&gt;Caveat (as of Oct 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt caching&lt;/td&gt;
&lt;td&gt;Up to 85% lower latency, 90% lower cost per AWS&lt;/td&gt;
&lt;td&gt;Long, stable system prompts, documents, tool schemas&lt;/td&gt;
&lt;td&gt;512–4,096-token minimum by model. 5-minute default TTL. Not for batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority tier&lt;/td&gt;
&lt;td&gt;Up to 25% better OTPS than Standard on most supported models, at a premium&lt;/td&gt;
&lt;td&gt;Customer-facing paths at peak&lt;/td&gt;
&lt;td&gt;Limited model list. Shares your on-demand quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex tier&lt;/td&gt;
&lt;td&gt;Discount for slower processing&lt;/td&gt;
&lt;td&gt;Evals, summarization, background agents&lt;/td&gt;
&lt;td&gt;Never on an interactive path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency-optimized inference&lt;/td&gt;
&lt;td&gt;&lt;code&gt;performanceConfig: {latency: "optimized"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nova Pro, Claude 3.5 Haiku, Llama 3.1 70B/405B&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Preview&lt;/strong&gt;. US Regions via cross-Region inference only. Falls back to standard past its quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distilled model&lt;/td&gt;
&lt;td&gt;Up to 500% faster, 75% cheaper, under 2% accuracy loss for RAG, per AWS&lt;/td&gt;
&lt;td&gt;High-volume, narrow tasks&lt;/td&gt;
&lt;td&gt;You need evals to prove quality held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intelligent Prompt Routing&lt;/td&gt;
&lt;td&gt;Routes each prompt to a cheaper or stronger model in one family&lt;/td&gt;
&lt;td&gt;Mixed easy and hard traffic&lt;/td&gt;
&lt;td&gt;English-optimized. Same-family models only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with prompt caching. The &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/04/amazon-bedrock-general-availability-prompt-caching/" rel="noopener noreferrer"&gt;launch figures&lt;/a&gt; are AWS maximums, so measure your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exact vs semantic caching
&lt;/h3&gt;

&lt;p&gt;Why not cache answers too? An exact cache, keyed on a hash of the normalized prompt, is safe but rarely hits on free text. A semantic cache answers when a past question's embedding is close. ElastiCache for Valkey 8.2 on node-based clusters has offered vector search since October 2025 at no extra charge, with up to 99% recall per AWS.&lt;/p&gt;

&lt;p&gt;The risk is a confident wrong hit, since "cancel my order" and "cancel my subscription" sit close in embedding space. Scope keys by tenant and intent, use a strict threshold and short TTLs, and never semantically cache user-specific answers.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Q["New question&amp;lt;br/&amp;gt;cancel my subscription"] --&amp;gt; E["Embed it and search&amp;lt;br/&amp;gt;past questions"]
    E --&amp;gt; N["Closest past question&amp;lt;br/&amp;gt;cancel my order"]
    N --&amp;gt;|"loose threshold, unscoped keys"| W["Confident wrong hit&amp;lt;br/&amp;gt;returns the order answer"]
    N --&amp;gt;|"strict threshold, keys scoped by tenant and intent"| M["Treated as a miss&amp;lt;br/&amp;gt;the model answers"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Two questions can sit close in embedding space and still need different answers, so the threshold and key scope decide what counts as a hit.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieval latency
&lt;/h3&gt;

&lt;p&gt;Retrieval lands in TTFT. In AWS's September 2026 benchmarks, OpenSearch measured p50 31 ms and p99 52 ms (1.2M vectors), Aurora PostgreSQL with pgvector about 32 ms and 89–94 ms, and S3 Vectors 294 ms and 506 ms (1M vectors). The workloads differed, so compare shape, not ranking (&lt;a href="https://aws.amazon.com/blogs/machine-learning/selecting-a-vector-store-for-amazon-bedrock-knowledge-bases/" rel="noopener noreferrer"&gt;AWS ML Blog, 2026&lt;/a&gt;). S3 Vectors holds 2 billion vectors per index at up to 90% lower cost (GA December 2025). It's a good full-corpus store behind a faster hot tier, and a poor fit for a 1-second TTFT budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent loops multiply everything
&lt;/h3&gt;

&lt;p&gt;Plan, three sequential tool calls, answer: at least five round-trips, each paying TTFT again. Parallelize independent tools, cap iterations and trace each step. CloudWatch generative AI observability (GA October 2025) traces model calls through Amazon Bedrock AgentCore operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure before you tune
&lt;/h3&gt;

&lt;p&gt;This prints all three budgets and the prompt cache result.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against &lt;a href="https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-runtime/client/converse_stream.html" rel="noopener noreferrer"&gt;boto3 converse_stream&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html" rel="noopener noreferrer"&gt;Bedrock prompt caching&lt;/a&gt; on Oct 28, 2026. Not deployed, so try it in a sandbox account first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ttft.py — TTFT, output tokens/sec and end-to-end latency for streamed Bedrock calls&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-runtime"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MODEL_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                  &lt;span class="c1"&gt;# a model or inference profile you can invoke&lt;/span&gt;
&lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"system_prompt.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# long, stable prefix: eligible for caching&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converse_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"cachePoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inferenceConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# an output cap is also a latency cap&lt;/span&gt;
        &lt;span class="n"&gt;serviceTier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SERVICE_TIER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# or "priority"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;"contentBlockDelta"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;"metadata"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"ttft_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"e2e_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"otps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"outputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"cache_read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cacheReadInputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"cache_write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cacheWriteInputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"What is the refund window?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Do you ship to Ghana?"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the second call should report cache_read &amp;gt; 0 if the prefix met the minimum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Bedrock's &lt;code&gt;TimeToFirstToken&lt;/code&gt; is low but your client-side TTFT is high, look at your own path.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI as an engineering aid
&lt;/h3&gt;

&lt;p&gt;AI tools help with the tedious parts: turning a trace into a hypothesis, drafting a k6 scenario, explaining a flame graph. CloudWatch investigations (GA June 2025, no additional cost) proposes root-cause hypotheses from your metrics, logs and traces.&lt;/p&gt;

&lt;p&gt;They also mislead predictably: caches with no invalidation story, closed-model load tests, numbers from nowhere. Three rules help. Every proposed optimization ships with a before-and-after measurement from your environment. Generated load tests use arrival-rate executors and thresholds. Any number an assistant offers needs a link before it reaches a design doc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who owns what
&lt;/h2&gt;

&lt;p&gt;Performance slips when nobody owns it. The product owner is accountable for latency budgets per journey, which the architect writes. Developers own p95/p99 and TTFT instrumentation. SRE or platform is accountable for CI load tests (developers write them) and owns alarms and regression response.&lt;/p&gt;

&lt;p&gt;The AWS line moves with compute. On EC2 it handles hardware and hypervisor, and you own instance type, OS tuning, scaling and connection pools. ECS on Fargate takes hosts and OS off you, leaving task sizing, scaling and app code. Lambda runs environments and scaling mechanics, and you own memory, init code and concurrency settings. Bedrock handles model serving and tier capacity, and you own model choice, prompt and output length, streaming, caching and quotas.&lt;/p&gt;

&lt;p&gt;Put simply, the provider owns how fast a model generates. You own how many tokens you send and request, and how often you call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Monday checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Write one budget sentence (percentile, endpoint, load) for your most important user journey.&lt;/li&gt;
&lt;li&gt;[ ] Replace every average-latency alarm with p95 and p99 alarms.&lt;/li&gt;
&lt;li&gt;[ ] Check LCP, INP and CLS at p75 on mobile from field data.&lt;/li&gt;
&lt;li&gt;[ ] Remove every header, cookie and query string from CloudFront cache keys that the origin doesn't use.&lt;/li&gt;
&lt;li&gt;[ ] Compare &lt;code&gt;Init Duration&lt;/code&gt; in Lambda logs with your p99, and run Lambda Power Tuning on your busiest function.&lt;/li&gt;
&lt;li&gt;[ ] Add a k6 &lt;code&gt;constant-arrival-rate&lt;/code&gt; test with &lt;code&gt;p(95)&lt;/code&gt; thresholds to your pipeline.&lt;/li&gt;
&lt;li&gt;[ ] Chart &lt;code&gt;TimeToFirstToken&lt;/code&gt; and OTPS for every LLM feature, and confirm responses stream.&lt;/li&gt;
&lt;li&gt;[ ] Move stable instructions to the front of prompts, add a &lt;code&gt;cachePoint&lt;/code&gt;, and check &lt;code&gt;cacheReadInputTokens&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Comparisons need a ruler. A cache, tier or runtime option is only better against a budget, at a percentile, under a load. With one, the options here become decisions. DAX when DynamoDB reads repeat. Valkey when results are computed. SnapStart when INIT owns your tail. Streaming and prompt caching when an LLM is the slowest hop. Without it, you're tuning by feel.&lt;/p&gt;

&lt;p&gt;Vodafone's faster page was still slow, and it sold 8% more. You don't need to be fast in the abstract, just measurably faster where your user is waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick the percentile first. The architecture follows from it.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Series: Non-functional Application Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cloudsege/non-functional-application-requirements-compliance-1enk"&gt;Non-functional Application Requirements: Compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Availability (coming soon)&lt;/li&gt;
&lt;li&gt;Scalability (coming soon)&lt;/li&gt;
&lt;li&gt;Maintainability (coming soon)&lt;/li&gt;
&lt;li&gt;Cost (coming soon)&lt;/li&gt;
&lt;li&gt;Sustainability (coming soon)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Cover image: &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 – Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up, Availability:&lt;/strong&gt; a fast system that's down is infinitely slow, so we'll look at composite SLOs, static stability, and what happens when your model provider throttles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/case-studies/vodafone" rel="noopener noreferrer"&gt;Vodafone: A 31% improvement in LCP increased sales by 8% — web.dev, 2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/case-studies/milliseconds-make-millions" rel="noopener noreferrer"&gt;Milliseconds make millions — web.dev / Deloitte and 55 for Google, 2020&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/articles/defining-core-web-vitals-thresholds" rel="noopener noreferrer"&gt;Defining the Core Web Vitals metrics thresholds — web.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/articles/vitals" rel="noopener noreferrer"&gt;Web Vitals — web.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cacm.acm.org/research/the-tail-at-scale/" rel="noopener noreferrer"&gt;The Tail at Scale — Dean and Barroso, Communications of the ACM, 2013&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/06/aws-edge-location-nigeria/" rel="noopener noreferrer"&gt;AWS announces new edge location in Nigeria — AWS, June 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cache-hit-ratio.html" rel="noopener noreferrer"&gt;Increasing the proportion of requests served from CloudFront caches — AWS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html" rel="noopener noreferrer"&gt;Using managed cache policies — CloudFront docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html" rel="noopener noreferrer"&gt;Using Origin Shield — CloudFront docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions-choosing.html" rel="noopener noreferrer"&gt;Differences between CloudFront Functions and Lambda@Edge — CloudFront docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/compute/aws-lambda-standardizes-billing-for-init-phase/" rel="noopener noreferrer"&gt;AWS Lambda standardizes billing for INIT phase — AWS Compute Blog, 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html" rel="noopener noreferrer"&gt;Configure Lambda function memory — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html" rel="noopener noreferrer"&gt;Improving startup performance with Lambda SnapStart — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html" rel="noopener noreferrer"&gt;Configuring provisioned concurrency — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html" rel="noopener noreferrer"&gt;Lambda Managed Instances — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html" rel="noopener noreferrer"&gt;Response streaming for Lambda functions — Lambda docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-target-group-attributes.html" rel="noopener noreferrer"&gt;Edit target group attributes for your Application Load Balancer — ELB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/WhatIs.html" rel="noopener noreferrer"&gt;What is Amazon ElastiCache? — ElastiCache docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/in-transit-encryption.html" rel="noopener noreferrer"&gt;ElastiCache in-transit encryption — ElastiCache docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/10/amazon-elasticache-vector-search/" rel="noopener noreferrer"&gt;Announcing vector search for Amazon ElastiCache — AWS, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" rel="noopener noreferrer"&gt;What is Amazon DynamoDB? — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.html" rel="noopener noreferrer"&gt;In-memory acceleration with DynamoDB Accelerator (DAX) — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.consistency.html" rel="noopener noreferrer"&gt;DAX and DynamoDB consistency models — DynamoDB docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/solutions/distributed-load-testing-on-aws/" rel="noopener noreferrer"&gt;Distributed Load Testing on AWS — AWS Solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/" rel="noopener noreferrer"&gt;Open and closed models — Grafana k6 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/k6/latest/using-k6/thresholds/" rel="noopener noreferrer"&gt;Thresholds — Grafana k6 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate/" rel="noopener noreferrer"&gt;Constant arrival rate — Grafana k6 docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://valkey-py.readthedocs.io/en/latest/connections.html" rel="noopener noreferrer"&gt;valkey-py connections — valkey-py docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html" rel="noopener noreferrer"&gt;CloudWatch Embedded Metric Format specification — CloudWatch docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/boto3/latest/reference/services/dynamodb/client/get_item.html" rel="noopener noreferrer"&gt;boto3 DynamoDB get_item — boto3 reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-observability-ttft-quota" rel="noopener noreferrer"&gt;Amazon Bedrock now supports observability of First Token Latency and Quota Consumption — AWS, March 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring-runtime-otps.html" rel="noopener noreferrer"&gt;Diagnose InvocationLatency increases using output tokens per second — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html" rel="noopener noreferrer"&gt;Amazon API Gateway REST API quotas — API Gateway docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-quotas.html" rel="noopener noreferrer"&gt;Amazon API Gateway HTTP API quotas — API Gateway docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/response-transfer-mode.html" rel="noopener noreferrer"&gt;Stream the integration response for your proxy integrations — API Gateway docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/04/amazon-bedrock-general-availability-prompt-caching/" rel="noopener noreferrer"&gt;Amazon Bedrock announces general availability of prompt caching — AWS, April 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html" rel="noopener noreferrer"&gt;Prompt caching for faster model inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-bedrock-priority-flex-inference-service-tiers" rel="noopener noreferrer"&gt;Amazon Bedrock introduces Priority and Flex inference service tiers — AWS, November 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html" rel="noopener noreferrer"&gt;Service tiers for model inference — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/latency-optimized-inference.html" rel="noopener noreferrer"&gt;Optimize model inference for latency — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/05/amazon-bedrock-model-distillation-generally-available" rel="noopener noreferrer"&gt;Amazon Bedrock Model Distillation is now generally available — AWS, May 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-routing.html" rel="noopener noreferrer"&gt;Intelligent prompt routing — Bedrock docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/selecting-a-vector-store-for-amazon-bedrock-knowledge-bases/" rel="noopener noreferrer"&gt;Selecting a vector store for Amazon Bedrock Knowledge Bases — AWS Machine Learning Blog, September 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-s3-vectors-generally-available/" rel="noopener noreferrer"&gt;Amazon S3 Vectors is now generally available — AWS, December 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/10/generative-ai-observability-amazon-cloudwatch" rel="noopener noreferrer"&gt;Generative AI observability now generally available for Amazon CloudWatch — AWS, October 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/06/ga-accelerate-troubleshooting-amazon-cloudwatch-investigations/" rel="noopener noreferrer"&gt;Now in GA: Amazon CloudWatch investigations — AWS, June 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-runtime/client/converse_stream.html" rel="noopener noreferrer"&gt;boto3 bedrock-runtime converse_stream — boto3 reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>ai</category>
      <category>performance</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Compliance</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Tue, 25 Nov 2025 12:58:58 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-compliance-1enk</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-compliance-1enk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Compliance isn't just a checkbox. It's the difference between a thriving business and a company facing €1.2 billion in fines.&lt;/strong&gt; In 2023, Meta paid that staggering amount for GDPR violations related to improperly transferring user data from the EU to the U.S. Meanwhile, healthcare organizations faced $4.75 million settlements for HIPAA failures, and retailers continued paying for breaches years after they occurred.&lt;/p&gt;

&lt;p&gt;Understanding compliance isn't optional anymore. It's the foundation of sustainable software development. Whether you're building a healthcare app, processing payments, or launching a SaaS platform, compliance requirements shape your architecture, influence your cloud provider choices, and directly impact your bottom line.&lt;/p&gt;

&lt;p&gt;This article breaks down what compliance really means, why it matters more than ever, and exactly how to implement it using AWS services before you become the next cautionary tale.&lt;/p&gt;




&lt;h2&gt;
  
  
  What: Understanding Compliance Frameworks and Requirements
&lt;/h2&gt;

&lt;p&gt;Compliance represents your legal and contractual obligation to handle data according to specific regulatory standards. Unlike optional best practices, &lt;strong&gt;compliance failures result in mandatory fines, lawsuits, and business shutdowns&lt;/strong&gt;. Modern applications typically need to comply with multiple frameworks simultaneously. A healthcare SaaS might need HIPAA, GDPR, and SOC 2 compliance all at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Major Compliance Frameworks
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. GDPR (General Data Protection Regulation)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; Any organization processing personal data of EU residents, regardless of company location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lawful Basis for Processing:&lt;/strong&gt; You must have a valid legal reason to process personal data (consent, contract, legitimate interest, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Subject Rights:&lt;/strong&gt; Users can request access, deletion, portability, and rectification of their data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by Design:&lt;/strong&gt; Data protection must be built into systems from the start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breach Notification:&lt;/strong&gt; Must notify authorities within 72 hours of discovering a breach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Protection Officer (DPO):&lt;/strong&gt; Required for organizations processing data at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Violation:&lt;/strong&gt; In May 2023, &lt;a href="https://www.dataprotection.ie/en/news-media/press-releases/Data-Protection-Commission-announces-conclusion-of-inquiry-into-Meta-Ireland" rel="noopener noreferrer"&gt;Ireland's Data Protection Commission fined Meta €1.2 billion&lt;/a&gt; for transferring EU user data to the U.S. without adequate safeguards. This remains the &lt;a href="https://www.edpb.europa.eu/news/news/2023/12-billion-euro-fine-facebook-result-edpb-binding-decision_en" rel="noopener noreferrer"&gt;largest GDPR fine ever imposed&lt;/a&gt;. More recently, in 2024, the &lt;a href="https://www.autoriteitpersoonsgegevens.nl/en/current/dutch-dpa-imposes-a-fine-of-290-million-euro-on-uber-because-of-transfers-of-drivers-data-to-the-us" rel="noopener noreferrer"&gt;Dutch DPA fined Uber €290 million&lt;/a&gt; for transferring sensitive driver data from the EU to the US without proper transfer mechanisms after they stopped using Standard Contractual Clauses in 2021.&lt;/p&gt;




&lt;h4&gt;
  
  
  2. HIPAA (Health Insurance Portability and Accountability Act)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; U.S. healthcare providers, health plans, healthcare clearinghouses, and their business associates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Rule:&lt;/strong&gt; Establishes standards for protecting individuals' medical records and personal health information (PHI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Rule:&lt;/strong&gt; Requires administrative, physical, and technical safeguards to protect electronic PHI (ePHI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breach Notification Rule:&lt;/strong&gt; Mandates notification to affected individuals, HHS, and sometimes the media&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Analysis:&lt;/strong&gt; Organizations must conduct thorough security risk assessments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Associate Agreements (BAAs):&lt;/strong&gt; Required contracts with any vendor handling PHI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Violation:&lt;/strong&gt; In February 2024, &lt;a href="https://www.healthcareitnews.com/news/montefiore-settles-ocr-475m-over-stolen-ephi" rel="noopener noreferrer"&gt;Montefiore Medical Center settled with HHS for $4.75 million&lt;/a&gt; following a &lt;a href="https://www.hipaajournal.com/montefiore-medical-center-malicious-insider-hipaa-penalty/" rel="noopener noreferrer"&gt;malicious insider cybersecurity investigation&lt;/a&gt;. The breach occurred when an employee stole and sold the protected health information of 12,517 patients. This case highlighted that threats don't just come from external hackers. Insider threats require equally rigorous controls.&lt;/p&gt;




&lt;h4&gt;
  
  
  3. PCI DSS (Payment Card Industry Data Security Standard)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; Any organization that stores, processes, or transmits cardholder data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build and Maintain a Secure Network:&lt;/strong&gt; Install firewalls and don't use vendor-supplied defaults&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect Cardholder Data:&lt;/strong&gt; Encrypt transmission and storage of cardholder data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain a Vulnerability Management Program:&lt;/strong&gt; Use and regularly update antivirus, develop secure systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement Strong Access Control Measures:&lt;/strong&gt; Restrict access on a need-to-know basis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regularly Monitor and Test Networks:&lt;/strong&gt; Track and monitor all access, regularly test security systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain an Information Security Policy:&lt;/strong&gt; Create comprehensive security policies for personnel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Violation:&lt;/strong&gt; In 2013, Target suffered one of the most infamous PCI compliance breaches when hackers stole 40 million credit card numbers. Despite having a $1.6 million malware detection tool in place, Target missed critical warnings for three weeks. The result? An &lt;a href="https://oag.ca.gov/news/press-releases/attorney-general-becerra-target-settles-record-185-million-credit-card-data" rel="noopener noreferrer"&gt;$18.5 million multi-state settlement&lt;/a&gt;, &lt;a href="https://www.nbcnews.com/business/business-news/target-settles-2013-hacked-customer-data-breach-18-5-million-n764031" rel="noopener noreferrer"&gt;over $202 million in total costs&lt;/a&gt;, and lasting reputational damage.&lt;/p&gt;




&lt;h4&gt;
  
  
  4. SOC 2 (System and Organization Controls 2)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; Service organizations that store customer data in the cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Requirements (Trust Service Criteria):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Protection against unauthorized access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability:&lt;/strong&gt; System accessibility as contractually agreed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing Integrity:&lt;/strong&gt; System processing is complete, valid, accurate, timely, and authorized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidentiality:&lt;/strong&gt; Information designated as confidential is protected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; Personal information is collected, used, retained, disclosed, and disposed of properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Business Impact:&lt;/strong&gt; Many enterprise organizations won't even consider vendors without SOC 2 Type II certification. A failed audit can mean losing major deals and damaging your market position.&lt;/p&gt;




&lt;h4&gt;
  
  
  5. ISO 27001
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; Any organization seeking to establish, implement, maintain, and continually improve an information security management system (ISMS).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Risk Assessment:&lt;/strong&gt; Identify and assess information security risks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Treatment:&lt;/strong&gt; Implement controls to address identified risks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; Maintain comprehensive documentation of the ISMS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal Audits:&lt;/strong&gt; Regular internal audits of the ISMS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Management Review:&lt;/strong&gt; Senior management oversight and commitment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Improvement:&lt;/strong&gt; Ongoing enhancement of the ISMS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Business Impact:&lt;/strong&gt; ISO 27001 certification is often required for government contracts and enterprise partnerships, particularly in Europe and internationally.&lt;/p&gt;




&lt;h4&gt;
  
  
  6. CCPA/CPRA (California Consumer Privacy Act/California Privacy Rights Act)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; Businesses collecting personal information of California residents meeting certain thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Right to Know:&lt;/strong&gt; Consumers can request what personal information is collected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right to Delete:&lt;/strong&gt; Consumers can request deletion of their personal information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right to Opt-Out:&lt;/strong&gt; Consumers can opt out of the sale of their personal information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right to Correct:&lt;/strong&gt; Consumers can request correction of inaccurate information (CPRA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right to Limit Use:&lt;/strong&gt; Consumers can limit use of sensitive personal information (CPRA)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidbf8zedfuu1r3ihendg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidbf8zedfuu1r3ihendg.png" alt="Compliance Failures Lead to Severe Consequences" width="800" height="623"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why: The Critical Importance of Compliance
&lt;/h2&gt;

&lt;p&gt;Security isn't just a "nice-to-have" feature. It's essential. Let's consider why a robust compliance strategy is non-negotiable:&lt;/p&gt;

&lt;h3&gt;
  
  
  Financial Impact
&lt;/h3&gt;

&lt;p&gt;The numbers tell a compelling story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;€5.88 billion:&lt;/strong&gt; Total GDPR fines issued since 2018 through 2025&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;€1.2 billion:&lt;/strong&gt; Single largest GDPR fine (Meta, 2023)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$4.88 million:&lt;/strong&gt; Average cost of a data breach globally in 2024 (IBM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$174,000:&lt;/strong&gt; Additional average cost for non-compliant organizations per breach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10%:&lt;/strong&gt; Year-over-year increase in breach costs, the highest total ever recorded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For payment processing, the stakes are equally high:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$5,000 - $100,000/month:&lt;/strong&gt; Typical PCI non-compliance penalties&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$50 - $90 per customer:&lt;/strong&gt; Additional fines per affected individual after a breach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoked merchant license:&lt;/strong&gt; The ultimate penalty, losing the ability to accept credit cards entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Trust and Reputation
&lt;/h3&gt;

&lt;p&gt;A single data breach can erode user trust for years. The reputational damage leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer churn and loss of market share&lt;/li&gt;
&lt;li&gt;Media fallout and public relations crises&lt;/li&gt;
&lt;li&gt;Difficulty attracting enterprise customers&lt;/li&gt;
&lt;li&gt;Challenges in recruiting top talent&lt;/li&gt;
&lt;li&gt;Investor concern and reduced valuations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When TikTok faced its &lt;a href="https://www.dataprotection.ie/en/news-media/latest-news/irish-data-protection-commission-fines-tiktok-eu530-million-and-orders-corrective-measures-following" rel="noopener noreferrer"&gt;€530 million GDPR fine in 2025&lt;/a&gt; for transferring EU user data to China without adequate safeguards, the damage extended far beyond the financial penalty. The &lt;a href="https://www.cnbc.com/2025/05/02/ireland-fines-tiktok-530-million-for-sending-eu-user-data-to-china.html" rel="noopener noreferrer"&gt;investigation revealed&lt;/a&gt; that engineers in China routinely accessed sensitive information of European users. That revelation intensified regulatory scrutiny and user concern globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Stability
&lt;/h3&gt;

&lt;p&gt;Insecure, non-compliant applications are more vulnerable to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cyber attacks that cause downtime and data loss&lt;/li&gt;
&lt;li&gt;Ransomware incidents that halt operations&lt;/li&gt;
&lt;li&gt;Regulatory investigations that consume resources&lt;/li&gt;
&lt;li&gt;Mandatory forensic examinations after breaches&lt;/li&gt;
&lt;li&gt;Court-ordered operational changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Compound Effect
&lt;/h3&gt;

&lt;p&gt;When compliance is compromised, it's not just a technical issue. It's a business crisis. Consider LinkedIn's 2024 experience: the &lt;a href="https://www.dataprotection.ie/en/news-media/press-releases/irish-data-protection-commission-fines-linkedin-ireland-eu310-million" rel="noopener noreferrer"&gt;Irish DPC fined them €310 million&lt;/a&gt; for &lt;a href="https://www.euronews.com/next/2024/10/24/irish-watchdog-slaps-linkedin-with-310m-fine-for-gdpr-violations" rel="noopener noreferrer"&gt;processing user data for behavioral analysis and targeted advertising&lt;/a&gt; without a valid legal basis. The company not only paid the fine but was ordered to bring its data processing into compliance, requiring significant technical and operational changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomnmsaf1udst2orjkp1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomnmsaf1udst2orjkp1l.png" alt="Non-Compliance Impacts Business Stability" width="799" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When: Embedding Compliance from Day One
&lt;/h2&gt;

&lt;p&gt;In today's rapid development environments, compliance needs to be part of the design and development process from the outset. The earlier compliance is embedded, the less costly it is to manage and mitigate issues later. This "shift-left" approach focuses on integrating compliance practices throughout the entire development lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Stages to Integrate Compliance
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Design Phase
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Flow Mapping:&lt;/strong&gt; Document what data you collect, where it's stored, how it flows through your system, and who has access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance Requirements Analysis:&lt;/strong&gt; Identify which frameworks apply to your business based on geography, industry, and data types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Impact Assessments:&lt;/strong&gt; Evaluate how new features or systems might affect user privacy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture Decisions:&lt;/strong&gt; Choose infrastructure and services that support compliance requirements (e.g., data residency)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Development Phase
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure Coding Practices:&lt;/strong&gt; Implement encryption, input validation, and secure authentication from the start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Minimization:&lt;/strong&gt; Collect only the data you actually need&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Controls:&lt;/strong&gt; Build role-based access control (RBAC) into your application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Logging:&lt;/strong&gt; Implement comprehensive logging of data access and modifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration as Code:&lt;/strong&gt; Use infrastructure as code to ensure consistent, compliant deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Testing and QA Phase
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compliance Testing:&lt;/strong&gt; Automated checks for compliance with relevant standards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Penetration Testing:&lt;/strong&gt; Regular security assessments to identify vulnerabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Protection Testing:&lt;/strong&gt; Verify encryption, access controls, and data handling procedures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Trail Verification:&lt;/strong&gt; Ensure all required events are being logged correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Deployment and Operations Phase
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Monitoring:&lt;/strong&gt; Real-time compliance posture assessment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Remediation:&lt;/strong&gt; Immediate response to compliance drift&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident Response Plans:&lt;/strong&gt; Documented procedures for breach notification and handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regular Audits:&lt;/strong&gt; Scheduled compliance assessments and certifications&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Cost of Delay
&lt;/h3&gt;

&lt;p&gt;Addressing compliance after the fact is exponentially more expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design phase fix:&lt;/strong&gt; ~1x cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development phase fix:&lt;/strong&gt; ~6x cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing phase fix:&lt;/strong&gt; ~15x cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production fix:&lt;/strong&gt; ~100x cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By shifting compliance left, you address potential issues early, reducing the risk of costly fixes, regulatory penalties, and reputational damage down the road.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkgibunfb2f79vtemtc01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkgibunfb2f79vtemtc01.png" alt="shifting compliance left" width="799" height="405"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where: Leveraging AWS Compliance Services
&lt;/h2&gt;

&lt;p&gt;AWS offers a comprehensive suite of services that cover different aspects of application compliance. Let's explore the key tools and how they work together to create a robust compliance infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS CloudTrail: Your Audit Log Foundation
&lt;/h3&gt;

&lt;p&gt;CloudTrail is the foundational service for compliance on AWS. It records all API activity across your AWS infrastructure, providing an immutable audit log that's essential for compliance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comprehensive Logging:&lt;/strong&gt; Captures API calls from the console, CLI, SDKs, and other AWS services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event History:&lt;/strong&gt; 90 days of management events free, with trails for longer retention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail Lake:&lt;/strong&gt; Enables SQL-based querying of event data for investigations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity Validation:&lt;/strong&gt; Log file integrity validation ensures logs haven't been tampered with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Compliance Value:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Satisfies audit logging requirements for HIPAA, PCI DSS, SOC 2, and GDPR&lt;/li&gt;
&lt;li&gt;Provides evidence for security investigations and incident response&lt;/li&gt;
&lt;li&gt;Enables detection of unauthorized access or suspicious activity
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: Creating a CloudTrail trail with log file validation&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;cloudtrail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cloudtrail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloudtrail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_trail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'compliance-trail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;S3BucketName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your-audit-logs-bucket'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;IncludeGlobalServiceEvents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;IsMultiRegionTrail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;EnableLogFileValidation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;KMSKeyId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your-kms-key-arn'&lt;/span&gt;  &lt;span class="c1"&gt;# Encrypt logs at rest&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  AWS Config: Continuous Configuration Assessment
&lt;/h3&gt;

&lt;p&gt;AWS Config continuously monitors and records your AWS resource configurations and allows you to automate the evaluation of configurations against desired compliance rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configuration Recording:&lt;/strong&gt; Tracks resource configurations over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config Rules:&lt;/strong&gt; Define rules that evaluate configurations against compliance requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conformance Packs:&lt;/strong&gt; Collections of rules mapped to compliance standards (PCI DSS, HIPAA, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Remediation:&lt;/strong&gt; Trigger automated fixes when resources drift from compliance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pre-Built Conformance Packs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Operational Best Practices for PCI DSS 3.2.1&lt;/li&gt;
&lt;li&gt;AWS Operational Best Practices for HIPAA&lt;/li&gt;
&lt;li&gt;AWS Operational Best Practices for GDPR&lt;/li&gt;
&lt;li&gt;AWS Operational Best Practices for CIS AWS Foundations Benchmark
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: AWS Config Rule for S3 bucket encryption
&lt;/span&gt;&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;S3BucketEncryptionRule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Config::ConfigRule&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ConfigRuleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3-bucket-server-side-encryption-enabled&lt;/span&gt;
      &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checks whether S3 buckets have server-side encryption enabled&lt;/span&gt;
      &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS&lt;/span&gt;
        &lt;span class="na"&gt;SourceIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED&lt;/span&gt;
      &lt;span class="na"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ComplianceResourceTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::Bucket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  AWS Security Hub: Centralized Security Posture Management
&lt;/h3&gt;

&lt;p&gt;Security Hub aggregates findings from multiple AWS security services and third-party tools, providing a comprehensive view of your security and compliance posture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated Security Checks:&lt;/strong&gt; Continuous compliance evaluation against industry standards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finding Aggregation:&lt;/strong&gt; Centralized view from GuardDuty, Inspector, Config, and third-party tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Score:&lt;/strong&gt; At-a-glance view of overall compliance status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Account Visibility:&lt;/strong&gt; Aggregate findings across your entire AWS organization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Supported Compliance Standards:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CIS AWS Foundations Benchmark (v1.2.0, v1.4.0, v3.0.0)&lt;/li&gt;
&lt;li&gt;AWS Foundational Security Best Practices&lt;/li&gt;
&lt;li&gt;PCI DSS v3.2.1&lt;/li&gt;
&lt;li&gt;NIST Special Publication 800-53 Revision 5
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: Enabling Security Hub with standards&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;securityhub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'securityhub'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Enable Security Hub&lt;/span&gt;
&lt;span class="n"&gt;securityhub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_security_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;EnableDefaultStandards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Enable specific standards&lt;/span&gt;
&lt;span class="n"&gt;securityhub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_enable_standards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;StandardsSubscriptionRequests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'StandardsArn'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.4.0'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'StandardsArn'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:securityhub:::ruleset/pci-dss/v/3.2.1'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  AWS Audit Manager: Automated Audit Evidence Collection
&lt;/h3&gt;

&lt;p&gt;Audit Manager helps you continuously audit your AWS usage and simplify how you assess risk and compliance with regulations and industry standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prebuilt Frameworks:&lt;/strong&gt; Ready-to-use frameworks for GDPR, HIPAA, PCI DSS, SOC 2, ISO 27001, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Evidence Collection:&lt;/strong&gt; Automatically gathers evidence from CloudTrail, Config, Security Hub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Frameworks:&lt;/strong&gt; Create frameworks tailored to your specific compliance needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assessment Reports:&lt;/strong&gt; Generate audit-ready reports for stakeholders and auditors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Evidence Types Collected:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration data from API calls&lt;/li&gt;
&lt;li&gt;User activity from CloudTrail logs&lt;/li&gt;
&lt;li&gt;Compliance check results from Security Hub and AWS Config&lt;/li&gt;
&lt;li&gt;Manual evidence uploads for documentation and procedures
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: Creating an assessment in Audit Manager&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;audit_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auditmanager'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create an assessment using the SOC 2 framework&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audit_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_assessment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'SOC2-Annual-Audit-2025'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Annual SOC 2 Type II audit assessment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;assessmentReportsDestination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;'destinationType'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'S3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'s3BucketName'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'your-audit-reports-bucket'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;'awsAccounts'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'123456789012'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'awsServices'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'serviceName'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'S3'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'serviceName'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'EC2'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'serviceName'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'RDS'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'serviceName'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Lambda'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'roleType'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'PROCESS_OWNER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'roleArn'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:iam::123456789012:role/AuditProcessOwner'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;frameworkId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'SOC2-framework-id'&lt;/span&gt;  &lt;span class="c1"&gt;# Use prebuilt SOC 2 framework&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  AWS Artifact: Compliance Documentation Hub
&lt;/h3&gt;

&lt;p&gt;AWS Artifact provides on-demand access to AWS compliance documentation and enables you to manage agreements with AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compliance Reports:&lt;/strong&gt; Access AWS SOC reports, PCI DSS attestation, ISO certifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agreements:&lt;/strong&gt; Accept and manage AWS Business Associate Agreement (BAA) for HIPAA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-Party Audit Reports:&lt;/strong&gt; Review independent auditor assessments of AWS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Service Access:&lt;/strong&gt; Download reports anytime for audit preparation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Available Reports Include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SOC 1, SOC 2, and SOC 3 Reports&lt;/li&gt;
&lt;li&gt;PCI DSS Attestation of Compliance&lt;/li&gt;
&lt;li&gt;ISO 27001, 27017, 27018, and 9001 Certifications&lt;/li&gt;
&lt;li&gt;FedRAMP Documentation&lt;/li&gt;
&lt;li&gt;HIPAA Eligible Services and BAA&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  AWS Macie: Automated Data Discovery and Protection
&lt;/h3&gt;

&lt;p&gt;Amazon Macie uses machine learning to automatically discover, classify, and protect sensitive data in AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated Discovery:&lt;/strong&gt; Continuously scans S3 buckets for sensitive data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classification:&lt;/strong&gt; Identifies PII, PHI, financial data, and custom data types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Alerts:&lt;/strong&gt; Generates findings when sensitive data is at risk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Inventory:&lt;/strong&gt; Provides visibility into what sensitive data exists and where&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Compliance Value:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports GDPR data mapping requirements&lt;/li&gt;
&lt;li&gt;Helps maintain HIPAA PHI inventories&lt;/li&gt;
&lt;li&gt;Identifies PCI DSS cardholder data exposure&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb7g4m1l88xq5or2n7e0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb7g4m1l88xq5or2n7e0h.png" alt="AWS Compliance Services Architecture" width="799" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 - CloudTrail (Foundation):&lt;/strong&gt; Records all API activity&lt;br&gt;
&lt;strong&gt;Layer 2 - AWS Config (Configuration):&lt;/strong&gt; Monitors resource configurations against rules&lt;br&gt;
&lt;strong&gt;Layer 3 - Security Hub (Dashboard):&lt;/strong&gt; Aggregates findings and provides compliance scores&lt;br&gt;
&lt;strong&gt;Layer 4 - Audit Manager (Evidence):&lt;/strong&gt; Collects evidence and generates audit reports&lt;/p&gt;




&lt;h2&gt;
  
  
  Who: The Roles and Responsibilities in Compliance
&lt;/h2&gt;

&lt;p&gt;Compliance is a shared responsibility. It's not just within your organization but also with your cloud provider. Understanding AWS's Shared Responsibility Model is crucial for compliance success.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AWS Shared Responsibility Model
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AWS is responsible for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Physical security of data centers&lt;/li&gt;
&lt;li&gt;Infrastructure hardware and software&lt;/li&gt;
&lt;li&gt;Network infrastructure&lt;/li&gt;
&lt;li&gt;Virtualization layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You are responsible for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data encryption and protection&lt;/li&gt;
&lt;li&gt;Identity and access management&lt;/li&gt;
&lt;li&gt;Network traffic protection&lt;/li&gt;
&lt;li&gt;Application security&lt;/li&gt;
&lt;li&gt;Operating system and platform configuration&lt;/li&gt;
&lt;li&gt;Compliance with specific regulations for your industry&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Roles Within Your Organization
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Compliance Officers
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Define compliance requirements and policies&lt;/li&gt;
&lt;li&gt;Coordinate with legal and regulatory bodies&lt;/li&gt;
&lt;li&gt;Oversee audit processes and certifications&lt;/li&gt;
&lt;li&gt;Report to executive leadership on compliance posture&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cloud Architects and DevOps Engineers
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Design compliant architectures&lt;/li&gt;
&lt;li&gt;Implement security controls and automation&lt;/li&gt;
&lt;li&gt;Configure AWS compliance services&lt;/li&gt;
&lt;li&gt;Maintain infrastructure as code for consistent deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Developers
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Write secure, compliant code&lt;/li&gt;
&lt;li&gt;Implement data protection at the application level&lt;/li&gt;
&lt;li&gt;Follow secure SDLC practices&lt;/li&gt;
&lt;li&gt;Conduct code reviews with security in mind&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Security Analysts
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Monitor security alerts and compliance dashboards&lt;/li&gt;
&lt;li&gt;Investigate potential violations&lt;/li&gt;
&lt;li&gt;Respond to incidents&lt;/li&gt;
&lt;li&gt;Conduct risk assessments&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Data Protection Officers (where required)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Ensure GDPR compliance&lt;/li&gt;
&lt;li&gt;Handle data subject requests&lt;/li&gt;
&lt;li&gt;Conduct privacy impact assessments&lt;/li&gt;
&lt;li&gt;Advise on data protection matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F56ampbwbnn1cnavpz8oy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F56ampbwbnn1cnavpz8oy.png" alt="Who?" width="800" height="851"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a Compliance Culture
&lt;/h3&gt;

&lt;p&gt;Successful compliance requires more than tools and processes. It requires a culture where everyone understands their role:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Executive Sponsorship:&lt;/strong&gt; Compliance initiatives must have visible support from leadership&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training Programs:&lt;/strong&gt; Regular training on compliance requirements and procedures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear Ownership:&lt;/strong&gt; Defined responsibilities for each compliance domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incentive Alignment:&lt;/strong&gt; Ensure security and compliance goals are reflected in team objectives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Communication:&lt;/strong&gt; Regular updates on compliance status and changes&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Compliance Best Practices: Building a Resilient Application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Implement Defense in Depth
&lt;/h3&gt;

&lt;p&gt;Don't rely on a single control. Layer your compliance measures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network segmentation and encryption&lt;/li&gt;
&lt;li&gt;Application-level access controls&lt;/li&gt;
&lt;li&gt;Data encryption at rest and in transit&lt;/li&gt;
&lt;li&gt;Comprehensive logging and monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Automate Everything Possible
&lt;/h3&gt;

&lt;p&gt;Manual compliance processes don't scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use AWS Config conformance packs for automated evaluation&lt;/li&gt;
&lt;li&gt;Implement automated remediation for common issues&lt;/li&gt;
&lt;li&gt;Deploy infrastructure as code for consistent, compliant deployments&lt;/li&gt;
&lt;li&gt;Schedule automated compliance reports&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Maintain Comprehensive Documentation
&lt;/h3&gt;

&lt;p&gt;Auditors will ask for evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document all security policies and procedures&lt;/li&gt;
&lt;li&gt;Maintain records of training and awareness programs&lt;/li&gt;
&lt;li&gt;Keep evidence of regular reviews and updates&lt;/li&gt;
&lt;li&gt;Archive audit logs with integrity verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Plan for Breach Notification
&lt;/h3&gt;

&lt;p&gt;Hope for the best, prepare for the worst:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create incident response playbooks&lt;/li&gt;
&lt;li&gt;Define notification procedures for each regulation&lt;/li&gt;
&lt;li&gt;Maintain contact information for regulatory authorities&lt;/li&gt;
&lt;li&gt;Practice incident response with tabletop exercises&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Implement Data Minimization
&lt;/h3&gt;

&lt;p&gt;You can't lose what you don't have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect only the data you need&lt;/li&gt;
&lt;li&gt;Implement data retention policies&lt;/li&gt;
&lt;li&gt;Securely delete data when no longer needed&lt;/li&gt;
&lt;li&gt;Anonymize or pseudonymize where possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Regular Compliance Audits
&lt;/h3&gt;

&lt;p&gt;Don't wait for the external audit to find issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conduct internal audits quarterly&lt;/li&gt;
&lt;li&gt;Use AWS Audit Manager for continuous assessment&lt;/li&gt;
&lt;li&gt;Address findings promptly&lt;/li&gt;
&lt;li&gt;Track remediation progress&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Monitor Third-Party Risk
&lt;/h3&gt;

&lt;p&gt;Your compliance extends to your vendors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require compliance certifications from vendors&lt;/li&gt;
&lt;li&gt;Include compliance requirements in contracts&lt;/li&gt;
&lt;li&gt;Regular assessment of third-party security&lt;/li&gt;
&lt;li&gt;Maintain business associate agreements (BAAs) where required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dd66fme6gm27z1qjwjm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dd66fme6gm27z1qjwjm.png" alt="Building a Resilient Application" width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Compliance as a Competitive Advantage
&lt;/h2&gt;

&lt;p&gt;In an era where data breaches make headlines daily and regulatory scrutiny intensifies, compliance isn't just about avoiding fines. It's a competitive differentiator.&lt;/p&gt;

&lt;p&gt;Companies that prioritize compliance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Win enterprise deals&lt;/strong&gt; that require SOC 2 and ISO 27001 certifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand globally&lt;/strong&gt; with confidence in meeting GDPR and regional requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build customer trust&lt;/strong&gt; through demonstrated commitment to data protection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce risk&lt;/strong&gt; of costly breaches and regulatory actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate efficiently&lt;/strong&gt; with automated compliance processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS provides the tools to make compliance manageable and affordable. By combining CloudTrail for audit logging, AWS Config for continuous monitoring, Security Hub for centralized visibility, and Audit Manager for evidence collection, you can build a compliance program that scales with your business.&lt;/p&gt;

&lt;p&gt;Remember: The best time to implement compliance was when you started building. The second best time is now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3xme812dgflvcvoy3s5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3xme812dgflvcvoy3s5u.png" alt="Prioritize Compliance" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Cover Image:&lt;/strong&gt; &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 - Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of a series on Non-functional Application Requirements. Read the previous articles:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-an-introduction-2f74"&gt;Non-functional Application Requirements: An Introduction&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613"&gt;Non-functional Application Requirements: Security&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next up: Accessibility - Designing for Everyone&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>software</category>
      <category>developer</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Provisioning AWS Infrastructure using Terraform and GitHub Actions</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Thu, 02 Oct 2025 12:32:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/provisioning-aws-infrastructure-using-terraform-and-github-actions-40ei</link>
      <guid>https://dev.to/aws-builders/provisioning-aws-infrastructure-using-terraform-and-github-actions-40ei</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Availability, repeatability, and security are table stakes for production workloads. This guide provisions a baseline AWS stack with Terraform that's resilient to AZ failures, enforces least-privilege boundaries, integrates with CI/CD, and manages Terraform state with an S3 and DynamoDB backend. We'll deploy four Apache web servers across two availability zones with a Multi-AZ RDS database, all automated through GitHub Actions.&lt;/p&gt;

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

&lt;p&gt;Incidents rarely happen at convenient times. You want deterministic deployments, blast-radius isolation, and multi-AZ redundancy so failures degrade gracefully. When one availability zone experiences issues, your application continues running on servers in the healthy zone without any manual intervention.&lt;/p&gt;

&lt;p&gt;Equally important is proper Terraform state management. State must be remote rather than stored on a laptop, it must be locked to prevent concurrent modifications, it must be encrypted and versioned for security and recovery, and it should be accessible via IAM rather than passed around in Slack or email. This becomes critical when working in teams or using automated CI/CD pipelines because everyone needs access to the same source of truth about what infrastructure exists.&lt;/p&gt;

&lt;p&gt;This article covers both infrastructure resources and state management so you can run with confidence. We'll also set up a complete GitHub Actions pipeline that automatically detects changes to your Terraform code and deploys them, replacing the manual apply process with automated continuous deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;The architecture we're building includes these components working together to create a highly available system. We'll provision a VPC with public and private subnets spread across two availability zones in the London region. In the public subnets, we'll deploy four EC2 instances running Apache web servers, with two instances in each availability zone. An Application Load Balancer will distribute incoming traffic across these four servers, automatically routing requests away from any unhealthy instances.&lt;/p&gt;

&lt;p&gt;For the database tier, we'll create a Multi-AZ RDS MySQL instance that automatically maintains a standby replica in a different availability zone. If the primary database fails, RDS automatically promotes the standby to become the new primary without requiring any code changes. The database will live in private subnets with no internet access, protected by security groups that only allow connections from the web servers.&lt;/p&gt;

&lt;p&gt;For state management, we'll configure an S3 bucket with versioning and encryption to store the Terraform state file, along with a DynamoDB table that provides locking to prevent multiple people or automation pipelines from modifying the infrastructure simultaneously. Finally, we'll set up GitHub Actions workflows that automatically run terraform plan on pull requests so you can review changes, and terraform apply when changes merge to the main branch, giving you the same automation benefits that Jenkins provides but using GitHub's native platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjv4gs22yphyfkpke9vw9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjv4gs22yphyfkpke9vw9.png" alt="Architecture" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what we're deploying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Networking&lt;/strong&gt;: VPC, 2 public subnets, 2 private subnets, Internet Gateway, route tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute&lt;/strong&gt;: 4 Apache web servers across 2 availability zones in an Auto Scaling Group&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingress&lt;/strong&gt;: Application Load Balancer with health checks and automatic failover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: RDS MySQL Multi-AZ in isolated private subnets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Security groups scoped per role, encrypted storage, IMDSv2 enforcement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State&lt;/strong&gt;: S3 remote state with DynamoDB locking, versioned and encrypted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: GitHub Actions pipeline for automated terraform plan and apply&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: CloudWatch metrics and alarms&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, you'll need Terraform version 1.6 or higher installed on your local machine. You'll also need the AWS CLI configured with an IAM user or role that has permissions to create VPC, EC2, RDS, and S3 resources. While we'll create the S3 bucket and DynamoDB table for state management as our first step, you'll need initial AWS credentials to bootstrap that infrastructure.&lt;/p&gt;

&lt;p&gt;You should also have a GitHub account and a repository where you'll store your Terraform code. The GitHub Actions workflows will run directly in your repository, so you'll need to configure AWS credentials as GitHub Secrets to allow the automation to deploy infrastructure on your behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Remote State Backend (S3 + DynamoDB)
&lt;/h2&gt;

&lt;p&gt;The first thing we need to do is create the infrastructure that will manage our Terraform state. This is a one-time bootstrap process. We're creating an S3 bucket to store the state file and a DynamoDB table to provide state locking. The bucket will have versioning enabled so you can recover from accidental deletions or corrupted state, and we'll enforce encryption at rest using AES256. We're also blocking all public access to ensure the state file, which may contain sensitive information like database passwords, remains private.&lt;/p&gt;

&lt;p&gt;The DynamoDB table uses on-demand billing so you only pay for the lock operations that actually occur, which is minimal. Terraform will write a lock entry to this table whenever someone runs an apply or plan operation, preventing others from making concurrent changes that could corrupt your infrastructure.&lt;/p&gt;

&lt;p&gt;Create a file called &lt;code&gt;backend-bootstrap.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is a one-time setup file to create the S3 bucket and DynamoDB table&lt;/span&gt;
&lt;span class="c1"&gt;# After running this once, you can delete this file or move it to a separate directory&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# S3 bucket to store Terraform state files&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-state-prod-stack-eu-west-2"&lt;/span&gt;

  &lt;span class="c1"&gt;# Prevent accidental deletion of the state bucket&lt;/span&gt;
  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;prevent_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Enable versioning so we can recover from bad state changes&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Encrypt state files at rest for security&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_server_side_encryption_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;apply_server_side_encryption_by_default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sse_algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AES256"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Block all public access to the state bucket&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# DynamoDB table for state locking to prevent concurrent modifications&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_dynamodb_table"&lt;/span&gt; &lt;span class="s2"&gt;"lock"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-state-locks-prod-stack"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;

  &lt;span class="nx"&gt;attribute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this bootstrap process once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the bucket and table are created, you can delete this bootstrap file or move it to a separate directory. The state infrastructure is now ready to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Backend Configuration
&lt;/h2&gt;

&lt;p&gt;Now that we have our state storage infrastructure, we need to configure Terraform to use it. We'll create a backend configuration file for each environment. This separation allows you to have different state files for development, staging, and production environments, preventing changes in one environment from affecting others.&lt;/p&gt;

&lt;p&gt;Create a directory structure for your environments and add a backend configuration file. For production, create &lt;code&gt;envs/prod/backend.hcl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-state-prod-stack-eu-west-2"&lt;/span&gt;
&lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"envs/prod/global.tfstate"&lt;/span&gt;
&lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
&lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-state-locks-prod-stack"&lt;/span&gt;
&lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your main Terraform directory, create a &lt;code&gt;main.tf&lt;/code&gt; file and add the backend configuration block. Notice that we don't specify the actual bucket name here because we'll pass that in via the backend config file. This allows us to use the same Terraform code across multiple environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Require Terraform version 1.6 or higher&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.6"&lt;/span&gt;

  &lt;span class="c1"&gt;# Backend configuration for remote state storage&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize Terraform with the backend configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init &lt;span class="nt"&gt;-backend-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;envs/prod/backend.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will now store its state remotely in S3 and use DynamoDB for locking. You can verify this worked by checking that Terraform created a &lt;code&gt;.terraform&lt;/code&gt; directory with backend configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Variables
&lt;/h2&gt;

&lt;p&gt;We need to define variables for values that change between environments or that should not be hardcoded. Database credentials are particularly important to handle as variables because you never want to commit passwords to version control. The sensitive flag ensures these values won't appear in Terraform's output logs.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;variables.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_username"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Database administrator username"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Database administrator password"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Environment name used for resource tagging and naming"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS region for resource deployment"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EC2 instance type for web servers"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_instance_class"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS instance class"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;terraform.tfvars&lt;/code&gt; file with actual values. This file should never be committed to version control, so add it to your &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;db_username&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;
&lt;span class="nx"&gt;db_password&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YourSecurePasswordHere123!"&lt;/span&gt;
&lt;span class="nx"&gt;environment&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="nx"&gt;aws_region&lt;/span&gt;        &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
&lt;span class="nx"&gt;instance_type&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
&lt;span class="nx"&gt;db_instance_class&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this line to your &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform.tfvars
*.tfvars
.terraform/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For GitHub Actions, we'll pass these values as GitHub Secrets instead of using a tfvars file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Networking Infrastructure
&lt;/h2&gt;

&lt;p&gt;The networking layer is the foundation of your infrastructure. We're creating a VPC with a CIDR block that gives us over 65,000 possible IP addresses, which is more than enough for most applications. We're enabling DNS support and hostnames so that resources within the VPC can resolve each other by DNS names rather than having to use IP addresses.&lt;/p&gt;

&lt;p&gt;We'll create two public subnets and two private subnets, with one of each type in each availability zone. The public subnets will host the load balancer and web servers, while the private subnets will host the database. By spreading resources across two availability zones, we ensure that if one entire data center goes offline, our application continues running in the other.&lt;/p&gt;

&lt;p&gt;The Internet Gateway provides the connection point between our VPC and the internet. We'll create route tables that define how traffic flows. The public route table will direct internet-bound traffic to the Internet Gateway, while the private route table will have no internet route, keeping the database completely isolated.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;network.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Fetch available availability zones in the current region&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_availability_zones"&lt;/span&gt; &lt;span class="s2"&gt;"available"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"available"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Main VPC - this is the container for all our networking resources&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-vpc"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Internet Gateway provides internet access for public subnets&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"igw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-igw"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public Subnet 1 - hosts ALB and web servers in first AZ&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_availability_zones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-public-1"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;Type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public Subnet 2 - hosts ALB and web servers in second AZ&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public_2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_availability_zones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-public-2"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;Type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Private Subnet 1 - hosts RDS in first AZ (completely isolated)&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.10.0/24"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_availability_zones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-private-1"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;Type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Private"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Private Subnet 2 - hosts RDS in second AZ (completely isolated)&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private_2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.11.0/24"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_availability_zones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-private-2"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;Type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Private"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Route table for public subnets - routes internet traffic to Internet Gateway&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-public-rt"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Route that directs all internet-bound traffic to the Internet Gateway&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route"&lt;/span&gt; &lt;span class="s2"&gt;"public_internet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;destination_cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
  &lt;span class="nx"&gt;gateway_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;igw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Associate public subnet 1 with the public route table&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Associate public subnet 2 with the public route table&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public_2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Route table for private subnets - no internet route, completely isolated&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-private-rt"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Associate private subnet 1 with the private route table&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"private_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Associate private subnet 2 with the private route table&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"private_2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5. Security Groups
&lt;/h2&gt;

&lt;p&gt;Security groups act as virtual firewalls that control traffic to and from your resources. We're implementing a defense-in-depth strategy where each tier of the application can only communicate with the tiers it needs to. The load balancer accepts traffic from the internet, the web servers accept traffic only from the load balancer, and the database accepts traffic only from the web servers.&lt;/p&gt;

&lt;p&gt;This layered security approach means that even if someone discovers the IP address of a web server, they cannot connect to it directly because the security group will reject any traffic that doesn't originate from the load balancer. Similarly, the database is completely inaccessible except from the web servers, even though it exists in the same VPC.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;security-groups.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Security Group for Application Load Balancer&lt;/span&gt;
&lt;span class="c1"&gt;# Accepts HTTP and HTTPS from the internet, forwards to web servers&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-alb-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Security group for application load balancer"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="c1"&gt;# Allow HTTP from anywhere on the internet&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP from internet"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Allow HTTPS from anywhere on the internet&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTPS from internet"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Allow all outbound traffic so ALB can forward to web servers&lt;/span&gt;
  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow all outbound"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-alb-sg"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Security Group for Web Servers&lt;/span&gt;
&lt;span class="c1"&gt;# Only accepts HTTP from the load balancer, not directly from internet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-web-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Security group for web server instances"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="c1"&gt;# Only allow HTTP from the load balancer security group&lt;/span&gt;
  &lt;span class="c1"&gt;# This prevents direct access to web servers from the internet&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP from ALB only"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Allow all outbound for package updates and external API calls&lt;/span&gt;
  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow all outbound"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-web-sg"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Security Group for RDS Database&lt;/span&gt;
&lt;span class="c1"&gt;# Only accepts MySQL connections from web servers&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"database"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-db-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Security group for RDS database"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="c1"&gt;# Only allow MySQL from the web server security group&lt;/span&gt;
  &lt;span class="c1"&gt;# Database is completely inaccessible from the internet&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MySQL from web servers only"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-db-sg"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6. IAM Roles for EC2
&lt;/h2&gt;

&lt;p&gt;We need to create an IAM role that our EC2 instances will assume. This role grants permissions for AWS Systems Manager Session Manager, which allows you to connect to instances without needing SSH keys or opening port 22. This is a more secure approach because you don't have to manage SSH keys, and all session activity is logged in CloudTrail for audit purposes.&lt;/p&gt;

&lt;p&gt;Create an &lt;code&gt;iam.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# IAM role that EC2 instances will assume&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-ec2-role"&lt;/span&gt;

  &lt;span class="c1"&gt;# Trust policy allowing EC2 service to assume this role&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-ec2-role"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Attach AWS-managed policy for Systems Manager access&lt;/span&gt;
&lt;span class="c1"&gt;# This allows SSM Session Manager connections without SSH&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_ssm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Instance profile wraps the role so it can be attached to EC2 instances&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_instance_profile"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_profile"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-ec2-profile"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7. Load Balancer and Web Servers
&lt;/h2&gt;

&lt;p&gt;Now we'll create the Application Load Balancer and the Auto Scaling Group with four web servers. The load balancer will perform health checks on each web server, and if a server fails its health check, the load balancer automatically stops sending traffic to it until it becomes healthy again.&lt;/p&gt;

&lt;p&gt;The Auto Scaling Group will maintain exactly four instances running at all times, distributed evenly across the two availability zones. If an instance fails or is terminated, the Auto Scaling Group automatically launches a replacement. The user data script installs Apache and creates a simple HTML page that displays the hostname, allowing you to see which server is responding to each request.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;compute.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Fetch the latest Ubuntu 20.04 AMI&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ami"&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;owners&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"099720109477"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Canonical's AWS account ID&lt;/span&gt;

  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"virtualization-type"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hvm"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Application Load Balancer distributes traffic across web servers&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-alb"&lt;/span&gt;
  &lt;span class="nx"&gt;internal&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;subnets&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;enable_deletion_protection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-alb"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Target group defines the pool of web servers&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-tg"&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="c1"&gt;# Health check configuration&lt;/span&gt;
  &lt;span class="c1"&gt;# ALB will mark instances as unhealthy if they fail these checks&lt;/span&gt;
  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;healthy_threshold&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;unhealthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="nx"&gt;interval&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
    &lt;span class="nx"&gt;matcher&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-tg"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# HTTP listener on port 80&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_listener"&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"forward"&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Launch template defines the configuration for EC2 instances&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_launch_template"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-web-"&lt;/span&gt;
  &lt;span class="nx"&gt;image_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;

  &lt;span class="c1"&gt;# Attach IAM role for SSM access&lt;/span&gt;
  &lt;span class="nx"&gt;iam_instance_profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_instance_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Enforce IMDSv2 for enhanced security&lt;/span&gt;
  &lt;span class="c1"&gt;# This prevents SSRF attacks against the instance metadata service&lt;/span&gt;
  &lt;span class="nx"&gt;metadata_options&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;http_endpoint&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"enabled"&lt;/span&gt;
    &lt;span class="nx"&gt;http_tokens&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"required"&lt;/span&gt;
    &lt;span class="nx"&gt;http_put_response_hop_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;network_interfaces&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;associate_public_ip_address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# User data script installs Apache and creates a simple test page&lt;/span&gt;
  &lt;span class="nx"&gt;UserData&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;Fn&lt;/span&gt;&lt;span class="err"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;Base64&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;Sub&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;
            &lt;span class="c1"&gt;#!/bin/bash&lt;/span&gt;
            &lt;span class="nx"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;-e&lt;/span&gt;
            &lt;span class="nx"&gt;apt-get&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;
            &lt;span class="nx"&gt;apt-get&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;-y&lt;/span&gt; &lt;span class="nx"&gt;apache2&lt;/span&gt;
            &lt;span class="nx"&gt;systemctl&lt;/span&gt; &lt;span class="nx"&gt;enable&lt;/span&gt; &lt;span class="nx"&gt;apache2&lt;/span&gt;
            &lt;span class="nx"&gt;systemctl&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="nx"&gt;apache2&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;tag_specifications&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"instance"&lt;/span&gt;
    &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-web-server"&lt;/span&gt;
      &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Auto Scaling Group maintains 4 web servers across 2 AZs&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_group"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-asg"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_zone_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;target_group_arns&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Maintain exactly 4 instances (2 per AZ)&lt;/span&gt;
  &lt;span class="nx"&gt;desired_capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="nx"&gt;min_size&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="nx"&gt;max_size&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

  &lt;span class="c1"&gt;# Use ELB health checks so unhealthy instances are replaced&lt;/span&gt;
  &lt;span class="nx"&gt;health_check_type&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ELB"&lt;/span&gt;
  &lt;span class="nx"&gt;health_check_grace_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

  &lt;span class="nx"&gt;launch_template&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_launch_template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$Latest"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Name"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-web-instance"&lt;/span&gt;
    &lt;span class="nx"&gt;propagate_at_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Environment"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;propagate_at_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8. RDS Database
&lt;/h2&gt;

&lt;p&gt;The RDS database will be deployed in Multi-AZ mode, which means AWS automatically maintains a standby replica in a different availability zone. If the primary database fails, RDS promotes the standby to primary automatically, typically within sixty to one hundred twenty seconds. Your application continues working because the database endpoint DNS name stays the same, it just points to the new primary instance.&lt;/p&gt;

&lt;p&gt;The database will be completely isolated in the private subnets with no route to the internet. It can only be accessed from the web servers through the security group rules we configured earlier.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;database.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# DB subnet group defines which subnets RDS can use&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_subnet_group"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-db-subnet-group"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-db-subnet-group"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# RDS MySQL instance with Multi-AZ for high availability&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-mysql"&lt;/span&gt;

  &lt;span class="c1"&gt;# Database configuration&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mysql"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"8.0.40"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_instance_class&lt;/span&gt;
  &lt;span class="nx"&gt;allocated_storage&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
  &lt;span class="nx"&gt;storage_type&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gp3"&lt;/span&gt;
  &lt;span class="nx"&gt;storage_encrypted&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Multi-AZ creates standby replica in different AZ&lt;/span&gt;
  &lt;span class="nx"&gt;multi_az&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Network configuration&lt;/span&gt;
  &lt;span class="nx"&gt;db_subnet_group_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_subnet_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;publicly_accessible&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="c1"&gt;# Authentication&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_username&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&lt;/span&gt;

  &lt;span class="c1"&gt;# Backup configuration&lt;/span&gt;
  &lt;span class="nx"&gt;backup_retention_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="nx"&gt;backup_window&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"03:00-04:00"&lt;/span&gt;
  &lt;span class="nx"&gt;maintenance_window&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mon:04:00-mon:05:00"&lt;/span&gt;

  &lt;span class="c1"&gt;# Disable final snapshot for easier cleanup (change for production)&lt;/span&gt;
  &lt;span class="nx"&gt;skip_final_snapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Enable deletion protection in production&lt;/span&gt;
  &lt;span class="nx"&gt;deletion_protection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.environment}-mysql"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 9. Outputs
&lt;/h2&gt;

&lt;p&gt;Outputs display important information after Terraform completes. We'll output the load balancer DNS name, which is the URL you'll use to access your application, and the database endpoint for connecting your application to the database.&lt;/p&gt;

&lt;p&gt;Create an &lt;code&gt;outputs.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_dns_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DNS name of the Application Load Balancer"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"URL to access the application"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://${aws_lb.main.dns_name}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"db_endpoint"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS database endpoint"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"db_address"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS database address"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vpc_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VPC ID"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 10. GitHub Actions CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;Now we'll set up GitHub Actions to automatically deploy infrastructure changes. This replaces Jenkins from the original article but provides the same functionality. When you push changes to your Terraform code, GitHub Actions will automatically run terraform plan to show you what will change. When you merge a pull request to the main branch, it will automatically run terraform apply to deploy those changes.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/terraform.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Terraform&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CI/CD'&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;TF_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.6.0'&lt;/span&gt;
  &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eu-west-2'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Terraform'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="c1"&gt;# These permissions are needed for the GitHub token&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Terraform&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;terraform_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.TF_VERSION }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS Credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.AWS_REGION }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Format Check&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fmt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform fmt -check&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;init&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init -backend-config=envs/prod/backend.hcl&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Validate&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;validate&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform validate -no-color&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform plan -no-color -input=false \&lt;/span&gt;
            &lt;span class="s"&gt;-var="db_username=${{ secrets.DB_USERNAME }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="db_password=${{ secrets.DB_PASSWORD }}"&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment Plan on PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`&lt;/span&gt;

            &lt;span class="s"&gt;&amp;lt;details&amp;gt;&amp;lt;summary&amp;gt;Show Plan&amp;lt;/summary&amp;gt;&lt;/span&gt;

            &lt;span class="s"&gt;\`\`\`terraform&lt;/span&gt;
            &lt;span class="s"&gt;${{ steps.plan.outputs.stdout }}&lt;/span&gt;
            &lt;span class="s"&gt;\`\`\`&lt;/span&gt;

            &lt;span class="s"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;

            &lt;span class="s"&gt;*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;&lt;/span&gt;

            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body: output&lt;/span&gt;
            &lt;span class="s"&gt;})&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan Status&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.plan.outcome == 'failure'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform apply -auto-approve -input=false \&lt;/span&gt;
            &lt;span class="s"&gt;-var="db_username=${{ secrets.DB_USERNAME }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="db_password=${{ secrets.DB_PASSWORD }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a destroy workflow at &lt;code&gt;.github/workflows/terraform-destroy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Terraform&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Destroy'&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"destroy"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;confirm'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destroy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Destroy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Infrastructure'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify Confirmation&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.inputs.confirm != 'destroy'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Confirmation failed. You must type 'destroy' to proceed."&lt;/span&gt;
          &lt;span class="s"&gt;exit 1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Terraform&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;terraform_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.6.0'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS Credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-west-2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init -backend-config=envs/prod/backend.hcl&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Destroy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform destroy -auto-approve -input=false \&lt;/span&gt;
            &lt;span class="s"&gt;-var="db_username=${{ secrets.DB_USERNAME }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="db_password=${{ secrets.DB_PASSWORD }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting Up GitHub Secrets
&lt;/h3&gt;

&lt;p&gt;In your GitHub repository, go to Settings → Secrets and variables → Actions, and add these secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;: Your AWS access key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;: Your AWS secret key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DB_USERNAME&lt;/code&gt;: Database admin username (e.g., "admin")&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DB_PASSWORD&lt;/code&gt;: Database admin password&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These secrets allow GitHub Actions to deploy infrastructure on your behalf without exposing credentials in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11. Deployment Process
&lt;/h2&gt;

&lt;p&gt;Now that everything is set up, here's how to deploy your infrastructure. First, you'll create the remote state backend locally, then push your code to GitHub where the automated pipeline takes over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Setup
&lt;/h3&gt;

&lt;p&gt;First, create the state backend infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the bootstrap file and run it&lt;/span&gt;
terraform init
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the S3 bucket and DynamoDB table are created, update your main configuration to use the remote backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Initialize with remote backend&lt;/span&gt;
terraform init &lt;span class="nt"&gt;-backend-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;envs/prod/backend.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will ask if you want to migrate your local state to S3. Type "yes" to proceed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdov5tuxr8cyfdh3v9wh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdov5tuxr8cyfdh3v9wh4.png" alt="Deployed Instances" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3ghi80okqkk7r5oefrt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3ghi80okqkk7r5oefrt.png" alt="Apache Installed" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy via GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Commit and push your code to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial infrastructure setup"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions will automatically run terraform plan and terraform apply. You can watch the progress in the Actions tab of your repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making Changes
&lt;/h3&gt;

&lt;p&gt;To make infrastructure changes, create a feature branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; update-instance-type
&lt;span class="c"&gt;# Make your changes to the Terraform files&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Update instance type to t3.small"&lt;/span&gt;
git push origin update-instance-type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a pull request on GitHub. The GitHub Actions workflow will automatically run terraform plan and post the results as a comment on your PR. Review the plan to see exactly what will change. If everything looks good, merge the pull request. GitHub Actions will automatically run terraform apply to deploy your changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring Deployments
&lt;/h3&gt;

&lt;p&gt;You can view deployment progress in real-time by going to the Actions tab in your GitHub repository. Each workflow run shows all the steps and their outputs. If a deployment fails, you can see the exact error message and debug from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12. Testing Your Infrastructure
&lt;/h2&gt;

&lt;p&gt;Once deployment completes, you can test your infrastructure. Get the load balancer URL from the Terraform outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform output alb_url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit that URL in your browser. You should see the custom welcome page showing the instance ID and availability zone. Refresh the page multiple times and you'll notice the instance ID changes as the load balancer distributes requests across your four web servers.&lt;/p&gt;

&lt;p&gt;To verify Multi-AZ deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check Auto Scaling Group distribution&lt;/span&gt;
aws autoscaling describe-auto-scaling-groups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-scaling-group-names&lt;/span&gt; prod-asg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'AutoScalingGroups[0].Instances[*].[InstanceId,AvailabilityZone]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Check RDS Multi-AZ status&lt;/span&gt;
aws rds describe-db-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-instance-identifier&lt;/span&gt; prod-mysql &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'DBInstances[0].MultiAZ'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see two instances in each availability zone, and the RDS Multi-AZ status should return true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployed Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi874c6x507q5hf7ipfap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi874c6x507q5hf7ipfap.png" alt="Deployed Architecture" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;p&gt;Let me walk you through solutions to common problems you might encounter. If your web servers aren't showing up as healthy in the load balancer target group, first check that Apache is actually running on the instances. Connect via Systems Manager Session Manager and run systemctl status apache2 to verify. Check the security group rules to ensure the web server security group allows traffic from the load balancer security group on port eighty.&lt;/p&gt;

&lt;p&gt;If you can't connect to the database from your web servers, verify the security group rules allow MySQL traffic from the web server security group. Check that the database is in the available state using the RDS console. Verify that the web servers can resolve the database endpoint DNS name. Test connectivity using telnet or nc to the database endpoint on port 3306.&lt;/p&gt;

&lt;p&gt;If Terraform apply fails with state locking errors, someone else might be running Terraform at the same time. Wait for their operation to complete. If Terraform crashed and left a stale lock, you can forcefully unlock using terraform force-unlock followed by the lock ID shown in the error message. Use this carefully because unlocking while someone else is actively making changes can corrupt your state.&lt;/p&gt;

&lt;p&gt;If instances launch but immediately fail health checks, check the user data script logs at /var/log/cloud-init-output.log on the instance. The user data script might be failing, preventing Apache from starting. Verify that the instance can reach the internet to download packages by checking the route tables and internet gateway attachment.&lt;/p&gt;

&lt;p&gt;If your GitHub Actions pipeline fails, check that you've configured all the required secrets in GitHub. Verify that the AWS credentials have sufficient permissions to create all the resources. Check the Actions logs for specific error messages that will point you to the problem.&lt;/p&gt;

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

&lt;p&gt;You've now built a production-grade, highly available infrastructure on AWS using Terraform and GitHub Actions. This infrastructure can handle availability zone failures gracefully, automatically scales to meet demand, and deploys changes through an automated pipeline. The four Apache web servers distributed across two availability zones ensure your application remains available even when problems occur.&lt;/p&gt;

&lt;p&gt;The Multi-AZ RDS database provides automatic failover if the primary database fails, and the remote state management with S3 and DynamoDB ensures your team can collaborate safely on infrastructure changes. The GitHub Actions pipeline replaces manual terraform apply commands with automated deployments that happen consistently every time.&lt;/p&gt;

&lt;p&gt;This foundation gives you a solid starting point that you can evolve as your needs grow.&lt;/p&gt;

&lt;p&gt;If you would prefer to use Jenkins as the CI/CD tool, check &lt;a href="https://dev.to/aws-builders/provisioning-aws-infrastructure-using-terraform-and-jenkins-cicd-pgj"&gt;this&lt;/a&gt; out&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>aws</category>
      <category>database</category>
    </item>
    <item>
      <title>Building the Classic Jotto Word Puzzle Game with Amazon Q Developer CLI</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Sat, 21 Jun 2025 23:44:03 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-the-classic-jotto-word-puzzle-game-with-amazon-q-developer-cli-fi5</link>
      <guid>https://dev.to/aws-builders/building-the-classic-jotto-word-puzzle-game-with-amazon-q-developer-cli-fi5</guid>
      <description>&lt;h2&gt;
  
  
  Retro Journey: Building a Classic Jotto Word Puzzle Game
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Jotto" rel="noopener noreferrer"&gt;Jotto&lt;/a&gt; is a classic word-guessing game that predates modern digital games like Wordle. I chose to recreate this game because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It has simple yet engaging mechanics - guess a 5-letter word and receive feedback on how many letters match&lt;/li&gt;
&lt;li&gt;It offers a pure deduction challenge without revealing letter positions&lt;/li&gt;
&lt;li&gt;It has nostalgic value while still being relevant to today's word game enthusiasts&lt;/li&gt;
&lt;li&gt;The rules are straightforward but the gameplay requires strategy and vocabulary skills&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unlike Wordle, Jotto focuses purely on letter matching without position information, making it a different kind of deductive challenge that tests your ability to track and eliminate possibilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effective Prompting Techniques
&lt;/h2&gt;

&lt;p&gt;Throughout this project, I discovered several effective prompting techniques when working with AWS Q Developer:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Structured Component Creation
&lt;/h3&gt;

&lt;p&gt;Breaking down the application into specific components with clear responsibilities yielded better results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate the shell of a new React app for a word puzzle game called Jotto. Use Vite for setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxr3z90afnn2nzl66zlq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxr3z90afnn2nzl66zlq.png" alt="Structured Component Creation" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This initial prompt established the project structure and technology stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Iterative Refinement
&lt;/h3&gt;

&lt;p&gt;When facing issues with text visibility, I used specific problem descriptions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I can't see the texts in the guess and matching letters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prompted targeted CSS improvements rather than a complete redesign.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Feature-Based Expansion
&lt;/h3&gt;

&lt;p&gt;Adding new features was most effective when describing the user experience rather than implementation details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do you want to include a panel that explains how the game works? it can pop up when selected by a new user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Usability-Focused Improvements
&lt;/h3&gt;

&lt;p&gt;When I noticed usability issues, I asked direct questions about specific features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your guesses should automatically scroll when there is a new addition, do you not think?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do you want to fix the letter tracker though? It does not help me effectively
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farpfgcpeabk0l2gu6ppo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farpfgcpeabk0l2gu6ppo.png" alt="Fix Letter Tracker" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These prompts led to targeted functional improvements rather than just visual changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Role of AWS Q Developer
&lt;/h2&gt;

&lt;p&gt;For this project, I used AWS Q Developer to assist with various aspects of development. This tool helped me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate initial project scaffolding and component structures&lt;/li&gt;
&lt;li&gt;Implement complex game logic like the letter matching algorithm&lt;/li&gt;
&lt;li&gt;Create responsive CSS designs that work across different devices&lt;/li&gt;
&lt;li&gt;Debug issues in the letter tracker's deduction logic&lt;/li&gt;
&lt;li&gt;Refine the user experience with features like auto-scrolling and first-time user onboarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS Q Developer proved particularly valuable for rapidly prototyping ideas and implementing features that would have taken significantly longer to code manually. The tool’s ability to comprehend game mechanics and translate them into functional code significantly expedited the development process.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Tackled Classic Programming Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  State Management
&lt;/h3&gt;

&lt;p&gt;Q Developer implemented React state management patterns with a custom hook (&lt;code&gt;useJottoGame&lt;/code&gt;) that encapsulated game logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useJottoGame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSecretWord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGuesses&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;gameStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGameStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...other state variables&lt;/span&gt;

  &lt;span class="c1"&gt;// Game logic functions&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitGuess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gameStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizedGuess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countMatchingLetters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedGuess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;setGuesses&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;normalizedGuess&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matchCount&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;setAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the player won&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isCorrectGuess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedGuess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateScore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalScore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setGameStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;won&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Save high score&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newHighScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;finalScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;word&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nf"&gt;saveHighScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newHighScore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="c1"&gt;// Check if the player lost (used all attempts)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setGameStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startNewGame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newSecretWord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRandomWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sampleWords&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setSecretWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newSecretWord&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setGuesses&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
    &lt;span class="nf"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
    &lt;span class="nf"&gt;setGameStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Return state and functions&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;gameStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;highScores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;submitGuess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;startNewGame&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach cleanly separated concerns and made the game logic reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Algorithm Implementation
&lt;/h3&gt;

&lt;p&gt;The letter matching algorithm was implemented efficiently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countMatchingLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guessLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretWord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;matchCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;guessLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;matchCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matchCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using Sets for this operation was an elegant solution that handles the Jotto matching rules correctly. The algorithm efficiently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Converts both words to lowercase for case-insensitive comparison&lt;/li&gt;
&lt;li&gt;Creates Sets of unique letters from each word&lt;/li&gt;
&lt;li&gt;Counts how many letters from the guess appear in the secret word&lt;/li&gt;
&lt;li&gt;Returns the match count&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This implementation correctly follows Jotto rules where each unique letter is counted only once, regardless of how many times it appears in either word.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistent Storage
&lt;/h3&gt;

&lt;p&gt;For data persistence, I used localStorage to save game progress and high scores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadHighScores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedScores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jottoHighScores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedScores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setHighScores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedScores&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveHighScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updatedScores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;highScores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newScore&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Keep only top 10 scores&lt;/span&gt;

  &lt;span class="nf"&gt;setHighScores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedScores&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jottoHighScores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedScores&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Loads existing scores from localStorage when the game initializes&lt;/li&gt;
&lt;li&gt;Adds new scores to the list when the player wins&lt;/li&gt;
&lt;li&gt;Sorts scores in descending order to maintain a proper leaderboard&lt;/li&gt;
&lt;li&gt;Limits the leaderboard to the top 10 scores to prevent excessive storage use&lt;/li&gt;
&lt;li&gt;Persists the updated leaderboard back to localStorage&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Development Automation That Saved Time
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Project Scaffolding
&lt;/h3&gt;

&lt;p&gt;With the help of AWS Q Developer, I generated the complete project structure with a single prompt, saving hours of setup time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create vite@latest jotto-game &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Creating the directory structure&lt;/li&gt;
&lt;li&gt;Setting up the Vite configuration&lt;/li&gt;
&lt;li&gt;Generating component files&lt;/li&gt;
&lt;li&gt;Creating utility modules&lt;/li&gt;
&lt;li&gt;Setting up CSS files&lt;/li&gt;
&lt;li&gt;Implementing the game logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project was organized with a clean, maintainable structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jotto-game/
├── src/
│   ├── components/
│   │   ├── game/
│   │   │   ├── GameBoard.jsx
│   │   │   ├── GuessInput.jsx
│   │   │   ├── GuessResults.jsx
│   │   │   ├── LetterTracker.jsx
│   │   │   └── GameProgress.jsx
│   │   └── ui/
│   │       ├── HelpPanel.jsx
│   │       ├── ScoreBoard.jsx
│   │       └── FirstTimeExperience.jsx
│   ├── hooks/
│   │   └── useJottoGame.js
│   ├── utils/
│   │   └── wordUtils.js
│   ├── App.jsx
│   ├── App.css
│   ├── main.jsx
│   └── index.css
├── package.json
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. CSS Generation
&lt;/h3&gt;

&lt;p&gt;I developed comprehensive CSS that handled both styling and responsive design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Game Progress Styles */&lt;/span&gt;
&lt;span class="nc"&gt;.game-progress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.progress-info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.attempts-count&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--secondary-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.attempts-remaining&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#666&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.progress-bar-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e0e0e0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.progress-bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Responsive Design */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;768px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.jotto-app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.guess-input&lt;/span&gt; &lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.guess-input&lt;/span&gt; &lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saved significant time that would have been spent on manual CSS adjustments and testing. The CSS included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A consistent color scheme using CSS variables&lt;/li&gt;
&lt;li&gt;Responsive design for different screen sizes&lt;/li&gt;
&lt;li&gt;Animations for user feedback&lt;/li&gt;
&lt;li&gt;Accessibility considerations&lt;/li&gt;
&lt;li&gt;Consistent spacing and typography&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Component Creation
&lt;/h3&gt;

&lt;p&gt;The React components were built with proper structure, props, and state management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GuessResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"no-guesses"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Make your first guess!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Your Guesses&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results-container"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Guess&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Matching Letters&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;latest-guess&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-word"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-count"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-indicator"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-number"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-text"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;letter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;letters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-tip"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Keep guessing to narrow down the letters!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Compare your guesses to deduce the secret word.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This component demonstrates several best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conditional rendering for empty state&lt;/li&gt;
&lt;li&gt;Proper use of map() with key props&lt;/li&gt;
&lt;li&gt;Dynamic class names based on component state&lt;/li&gt;
&lt;li&gt;Semantic HTML with table for tabular data&lt;/li&gt;
&lt;li&gt;Conditional text content based on data values&lt;/li&gt;
&lt;li&gt;Nested component structure for better organization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Interesting Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Improved Letter Tracker Component with Logical Deduction
&lt;/h3&gt;

&lt;p&gt;One of the most interesting solutions was the enhanced Letter Tracker component that helps players track which letters they've used and make logical deductions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LetterTracker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create the alphabet&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alphabet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abcdefghijklmnopqrstuvwxyz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Track which letters have been used in guesses&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usedLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Track which letters are confirmed to be in the secret word&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmedLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Track which letters are confirmed NOT to be in the secret word&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Process all guesses to determine letter statuses using logical deduction&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// First, add all guessed letters to usedLetters&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guessLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;guessLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;usedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Find words with zero matches - all letters in these words are eliminated&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Find words with all 5 letters matching - all letters in these words are confirmed&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// For words with some matches, use deduction&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// For each letter in this guess&lt;/span&gt;
        &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// If we've seen this letter in a word with zero matches, it's eliminated&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Check if this letter appears in all guesses with matches&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;appearsInAllMatchingGuesses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;otherGuess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;otherIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;otherIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;otherGuess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;appearsInAllMatchingGuesses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="c1"&gt;// If this letter appears in all guesses with matches, it's likely in the word&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appearsInAllMatchingGuesses&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove any letters from confirmedLetters if they're also in eliminatedLetters&lt;/span&gt;
    &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Get letter status&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getLetterStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eliminated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;used&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tracker"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Letter Tracker&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-grid"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;alphabet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; 
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`letter-tile &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getLetterStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`Letter &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getLetterStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-legend"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile unused"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Unused&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile used"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Used (Unknown)&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile confirmed"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;In Word&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile eliminated"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Not in Word&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Get letter status&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getLetterStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;used&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tracker"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Letter Tracker&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-grid"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;alphabet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; 
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`letter-tile &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getLetterStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`Letter &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getLetterStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-legend"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile unused"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Unused&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile used"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Used in guesses&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"legend-item"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tile confirmed"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Likely in word&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This component cleverly uses Sets to track letter usage and provides visual feedback without revealing exact letter positions (keeping true to Jotto's mechanics). The implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a visual grid of all alphabet letters&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbamneb40l4aatos4tjou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbamneb40l4aatos4tjou.png" alt="Visual Grid of the Alphabets" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tracks which letters have been used in any guess&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpe15vrd48lq78nr8geq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpe15vrd48lq78nr8geq.png" alt="Letter Tracking" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Uses a probabilistic approach to suggest which letters might be in the secret word&lt;/li&gt;
&lt;li&gt;Provides a legend explaining the color coding&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96zd6sd4pkg18976bt1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96zd6sd4pkg18976bt1x.png" alt="Colour Coding" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Includes ARIA labels for accessibility&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. First-Time User Experience
&lt;/h3&gt;

&lt;p&gt;I created a sophisticated first-time user experience that detects new players and shows a welcome modal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FirstTimeExperience&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFirstVisit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsFirstVisit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showWelcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowWelcome&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check if user has visited before&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasVisitedBefore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jottoHasVisited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasVisitedBefore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// First time visitor&lt;/span&gt;
      &lt;span class="nf"&gt;setShowWelcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jottoHasVisited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setIsFirstVisit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;closeWelcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setShowWelcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setIsFirstVisit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;showWelcome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"welcome-overlay"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"welcome-modal"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Welcome to Jotto!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"welcome-content"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Jotto is a word guessing game where you try to figure out a secret 5-letter word.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;How to Play:&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Enter a 5-letter word as your guess&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;You'll be told how many letters from your guess appear in the secret word&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Use this information to make better guesses&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Try to find the secret word in as few attempts as possible&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"welcome-example"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Example:&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; If the secret word is "TRAIN" and you guess "PLANE", you'd get 3 matches (A, N, E).&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;You have 10 attempts to guess the word. Good luck!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"welcome-button"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;closeWelcome&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Start Playing
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Uses localStorage to detect first-time visitors&lt;/li&gt;
&lt;li&gt;Displays a modal overlay with game instructions&lt;/li&gt;
&lt;li&gt;Provides a clear example of how the game works&lt;/li&gt;
&lt;li&gt;Offers a prominent button to start playing&lt;/li&gt;
&lt;li&gt;Only shows once per user (unless they clear their browser data)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvoirjzqpoyqjfqnw1v2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvoirjzqpoyqjfqnw1v2t.png" alt="Welcome Modal" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Score Calculation Algorithm
&lt;/h3&gt;

&lt;p&gt;The scoring system uses a clever algorithm that rewards efficiency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calculateScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Base score: 1000 points&lt;/span&gt;
  &lt;span class="c1"&gt;// Subtract 50 points for each attempt&lt;/span&gt;
  &lt;span class="c1"&gt;// Minimum score is 100&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;penaltyPerAttempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calculatedScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseScore&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;penaltyPerAttempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;calculatedScore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Starts with a perfect score of 1000 points&lt;/li&gt;
&lt;li&gt;Deducts 50 points for each guess attempt&lt;/li&gt;
&lt;li&gt;Ensures a minimum score of 100 points even with many attempts&lt;/li&gt;
&lt;li&gt;Creates a scoring system that rewards efficiency but doesn't overly punish exploration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnw8rftjphavae6k8cfx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnw8rftjphavae6k8cfx3.png" alt="High Scores" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Game Progress Component
&lt;/h3&gt;

&lt;p&gt;The game progress component provides visual feedback on the player's progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GameProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progressPercentage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Determine color based on progress&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getProgressColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress-low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress-medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress-high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Get encouraging message based on progress&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Make your first guess!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Great start! Keep going.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You're making progress!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Getting closer!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Final attempts - think carefully!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"game-progress"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"progress-info"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"attempts-count"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Attempt &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; of &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"attempts-remaining"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;(&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; remaining)&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"progress-bar-container"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; 
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`progress-bar &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getProgressColor&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;progressPercentage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"progressbar"&lt;/span&gt;
          &lt;span class="na"&gt;aria-valuenow&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;aria-valuemin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
          &lt;span class="na"&gt;aria-valuemax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"progress-message"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Calculates progress as a percentage of attempts used&lt;/li&gt;
&lt;li&gt;Changes color based on how many attempts remain (green → yellow → red)&lt;/li&gt;
&lt;li&gt;Displays encouraging messages that change as the game progresses&lt;/li&gt;
&lt;li&gt;Includes proper ARIA attributes for accessibility&lt;/li&gt;
&lt;li&gt;Provides both visual and textual feedback&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Game Features and Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Game Mechanics
&lt;/h3&gt;

&lt;p&gt;The game follows traditional Jotto rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Players guess a 5-letter word&lt;/li&gt;
&lt;li&gt;After each guess, they're told how many letters from their guess appear in the secret word&lt;/li&gt;
&lt;li&gt;The position of matching letters doesn't matter&lt;/li&gt;
&lt;li&gt;Players have a limited number of attempts (10) to guess the word&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  User Interface Components
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Header Section&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Game title and brief description&lt;/li&gt;
&lt;li&gt;Help panel toggle button&lt;/li&gt;
&lt;li&gt;High scores panel toggle button&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrpfjco4m53nwpigm9lz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrpfjco4m53nwpigm9lz.png" alt="Header Section" width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Game Progress Indicator&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visual progress bar showing attempts used&lt;/li&gt;
&lt;li&gt;Text showing attempts remaining&lt;/li&gt;
&lt;li&gt;Encouraging messages based on progress
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhuc3vlpgfk822dbsgrfb.png" alt="Game Progress Component" width="800" height="169"&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Guess Input&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text input for entering 5-letter words&lt;/li&gt;
&lt;li&gt;Submit button&lt;/li&gt;
&lt;li&gt;Input validation (letters only, exactly 5 characters)&lt;/li&gt;
&lt;li&gt;Error messages for invalid input&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji32ku0tos1xqwvz7sj0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji32ku0tos1xqwvz7sj0.png" alt="Guess Input" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Guess Results Table&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;List of previous guesses&lt;/li&gt;
&lt;li&gt;Number of matching letters for each guess&lt;/li&gt;
&lt;li&gt;Highlighting for the most recent guess&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh441r50ppjmo7xs01aoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh441r50ppjmo7xs01aoj.png" alt="Guess Results Table" width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Letter Tracker&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Visual grid of all alphabet letters&lt;/li&gt;
&lt;li&gt;Color coding to show used and potentially matching letters&lt;/li&gt;
&lt;li&gt;Legend explaining the color codes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kl04tbo1eeojj7nrmt5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kl04tbo1eeojj7nrmt5.png" alt="Letter Tracker" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Help Panel&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Game rules explanation&lt;/li&gt;
&lt;li&gt;Strategy tips&lt;/li&gt;
&lt;li&gt;Example of gameplay&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i9eytz2ytp0z4haayg0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i9eytz2ytp0z4haayg0.png" alt="Help Panel" width="800" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Score Board&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Current score display&lt;/li&gt;
&lt;li&gt;High scores leaderboard&lt;/li&gt;
&lt;li&gt;Persistent storage of scores&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5exr267xmit8oguue79q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5exr267xmit8oguue79q.png" alt="Score Board" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First-Time User Experience&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Welcome modal for new players&lt;/li&gt;
&lt;li&gt;Game instructions&lt;/li&gt;
&lt;li&gt;Example of gameplay&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Technical Implementation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;React Components and Hooks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Functional components with React hooks&lt;/li&gt;
&lt;li&gt;Custom game logic hook (useJottoGame)&lt;/li&gt;
&lt;li&gt;Component composition for UI organization&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;State Management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React useState for component state&lt;/li&gt;
&lt;li&gt;Custom hook for game state&lt;/li&gt;
&lt;li&gt;Props for component communication&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS variables for consistent theming&lt;/li&gt;
&lt;li&gt;Responsive design with media queries&lt;/li&gt;
&lt;li&gt;Animations for user feedback&lt;/li&gt;
&lt;li&gt;Accessibility considerations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Persistence&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;localStorage for saving high scores&lt;/li&gt;
&lt;li&gt;localStorage for tracking first-time visitors&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Game Logic&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Word matching algorithm&lt;/li&gt;
&lt;li&gt;Score calculation&lt;/li&gt;
&lt;li&gt;Win/lose condition checking&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Building this Jotto word puzzle game demonstrated how effective modern development tools can be for rapid prototyping and development. With AWS Q Developer's assistance, I successfully:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generated a complete React application structure&lt;/li&gt;
&lt;li&gt;Implemented game logic following classic Jotto rules&lt;/li&gt;
&lt;li&gt;Created responsive UI components with proper accessibility&lt;/li&gt;
&lt;li&gt;Added sophisticated features like score tracking and first-time user experience&lt;/li&gt;
&lt;li&gt;Provided iterative improvements based on feedback&lt;/li&gt;
&lt;li&gt;Implemented intelligent deduction tools like the enhanced letter tracker&lt;/li&gt;
&lt;li&gt;Added usability features like auto-scrolling guess results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final product is a fully functional Jotto game that maintains the classic gameplay while offering modern UI features and responsive design. This project shows how modern development tools can help resurrect classic games for modern platforms while preserving their original charm and gameplay mechanics.&lt;/p&gt;

&lt;p&gt;The development process highlighted several strengths of using AWS Q Developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapid prototyping and iteration&lt;/li&gt;
&lt;li&gt;Comprehensive implementation of features&lt;/li&gt;
&lt;li&gt;Attention to details like accessibility and responsive design&lt;/li&gt;
&lt;li&gt;Ability to generate both functional code and visual styling&lt;/li&gt;
&lt;li&gt;Capacity to implement complex game logic and algorithms&lt;/li&gt;
&lt;li&gt;Adaptability to changing requirements and feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This retro journey demonstrates that modern development tools can be powerful allies for game development, especially when reviving classic games with modern implementations. The ability to quickly iterate on features and improve usability based on feedback makes tools like AWS Q Developer invaluable partners in the development process.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. UI Evaluation Requests
&lt;/h3&gt;

&lt;p&gt;Asking for UI evaluation led to comprehensive improvements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What do you think of the user interface as is? is it standard? would it help users play the game appropriately?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This yielded thoughtful analysis and specific enhancement suggestions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Auto-Scrolling Guess Results
&lt;/h3&gt;

&lt;p&gt;The GuessResults component was enhanced with auto-scrolling functionality to improve user experience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GuessResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create a ref for the container to enable auto-scrolling&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultsContainerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Auto-scroll to the bottom when new guesses are added&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultsContainerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;resultsContainerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resultsContainerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"no-guesses"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Make your first guess!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Your Guesses&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results-container"&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;resultsContainerRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Guess&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Matching Letters&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;latest-guess&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-word"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-count"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-indicator"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-number"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"match-text"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;letter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;letters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-tip"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Keep guessing to narrow down the letters!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Compare your guesses to deduce the secret word.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Uses React's &lt;code&gt;useRef&lt;/code&gt; hook to create a reference to the scrollable container&lt;/li&gt;
&lt;li&gt;Implements &lt;code&gt;useEffect&lt;/code&gt; to automatically scroll to the bottom whenever new guesses are added&lt;/li&gt;
&lt;li&gt;Ensures the most recent guess is always visible without requiring manual scrolling&lt;/li&gt;
&lt;li&gt;Improves user experience by keeping the focus on the latest game information
## Fixing the Letter Tracker&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During testing, we discovered issues with the letter tracker component: it was incorrectly identifying letters as being in the secret word. After multiple iterations, we implemented a sophisticated deduction algorithm that properly follows Jotto's logical principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Earlier implementations of the letter tracker were either too aggressive (showing too many letters as confirmed) or too conservative (not providing enough useful information). The challenge was to create a system that makes logical deductions similar to how a human player would approach the game.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Advanced Deduction Logic
&lt;/h3&gt;

&lt;p&gt;We implemented a comprehensive deduction algorithm that applies multiple logical rules iteratively until no more progress can be made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Process all guesses to determine letter statuses using logical deduction&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Eliminate letters from guesses with zero matches&lt;/span&gt;
  &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2: Confirm letters from guesses with all 5 letters matching&lt;/span&gt;
  &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 3: Apply more sophisticated deduction logic&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;madeProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Keep iterating as long as we're making progress in our deductions&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;madeProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;madeProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// For each guess, check if we can deduce more information&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guessLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Count confirmed and eliminated letters in this guess&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;confirmedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;eliminatedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nx"&gt;guessLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;confirmedCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;eliminatedCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Case 3: If all letters except X are eliminated, and we need X more matches,&lt;/span&gt;
        &lt;span class="c1"&gt;// then those X letters must be in the word&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonEliminatedNonConfirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guessLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonEliminatedNonConfirmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;confirmedCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;nonEliminatedNonConfirmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
              &lt;span class="nx"&gt;madeProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Case 4: If we've confirmed as many letters as there are matches,&lt;/span&gt;
        &lt;span class="c1"&gt;// all other letters in this guess must be eliminated&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirmedCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;guessLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;confirmedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
              &lt;span class="nx"&gt;madeProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Features of the Improved Algorithm:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Iterative Deduction Process&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The algorithm repeatedly applies deduction rules until no more progress can be made&lt;/li&gt;
&lt;li&gt;This mimics how a human player would gradually eliminate possibilities&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multiple Deduction Rules&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic rules: Letters in zero-match guesses are eliminated; letters in full-match guesses are confirmed&lt;/li&gt;
&lt;li&gt;Advanced rule 1: If we need X more matches and only X letters remain non-eliminated, those letters must be in the word&lt;/li&gt;
&lt;li&gt;Advanced rule 2: If we've confirmed as many letters as there are matches, all other letters must be eliminated&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Logical Consistency Checks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The algorithm checks for and handles contradictions in the deduction process&lt;/li&gt;
&lt;li&gt;Final cleanup ensures no letter is marked as both confirmed and eliminated&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This improved implementation provides accurate feedback to the player, making the letter tracker a truly useful tool for deduction in the Jotto game, while maintaining the core Jotto gameplay that's distinct from Wordle's position-based hints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhancing the User Experience for Jotto Mechanics
&lt;/h2&gt;

&lt;p&gt;After testing the game, we realized that players might not fully understand how Jotto differs from more familiar word games like Wordle. In Jotto, players only learn how many letters match, not which specific letters. This creates a different kind of deduction challenge that might not be immediately intuitive to new players.&lt;/p&gt;

&lt;p&gt;To address this, we implemented three key improvements:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Simplified Letter Tracker with Clear Help Text
&lt;/h3&gt;

&lt;p&gt;We redesigned the letter tracker to focus on what can be known with certainty in Jotto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LetterTracker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowHelp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Track which letters have been used in guesses&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usedLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Track which letters are definitely eliminated (not in the word)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Process all guesses to determine letter statuses&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Add all guessed letters to usedLetters&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guessLetters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;guessLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;usedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Eliminate letters from guesses with zero matches&lt;/span&gt;
    &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Get letter status&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getLetterStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eliminatedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eliminated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usedLetters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;used&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tracker"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tracker-header"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Letter Tracker&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; 
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"help-toggle-button"&lt;/span&gt;
          &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleHelp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;aria-expanded&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hide Help&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"letter-tracker-help"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;In Jotto, you need to deduce which letters are in the secret word:&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Red letters&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; are definitely &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;not&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; in the word (from guesses with 0 matches)&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Blue letters&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; have been used in guesses but could be in the word&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Gray letters&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; haven't been used yet&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Use the match counts from your guesses to deduce which blue letters might be in the word!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Letter grid and legend */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focuses on what can be known with certainty (eliminated letters)&lt;/li&gt;
&lt;li&gt;Uses clearer color coding (blue for used letters, red for eliminated)&lt;/li&gt;
&lt;li&gt;Includes a help button with explanatory text&lt;/li&gt;
&lt;li&gt;Adds a deduction tip to remind players of Jotto's mechanics&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Enhanced Guess Results with Contextual Help
&lt;/h3&gt;

&lt;p&gt;We added help text to the guess results component to explain how to interpret match counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GuessResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowHelp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Toggle help text&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggleHelp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setShowHelp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results-header"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Your Guesses&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; 
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"help-toggle-button"&lt;/span&gt;
          &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleHelp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;aria-expanded&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hide Help&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showHelp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-results-help"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;In Jotto, after each guess you'll see how many letters from your guess appear in the secret word.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;For example, if the secret word is "TRAIN" and you guess "PLANE", you'd get 3 matches (A, N, E).&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;The position of the letters doesn't matter - only whether they appear in the word.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Guess results table */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"guess-tip"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The number shows how many letters from your guess appear in the secret word.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Compare your guesses to deduce which letters are in the secret word.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Adds a help button with contextual explanations&lt;/li&gt;
&lt;li&gt;Provides a concrete example of how matching works&lt;/li&gt;
&lt;li&gt;Includes a tip that changes based on game progress&lt;/li&gt;
&lt;li&gt;Emphasizes the need to compare guesses for deduction&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Improved Help Panel with Jotto vs. Wordle Comparison
&lt;/h3&gt;

&lt;p&gt;We expanded the help panel to clearly explain how Jotto differs from Wordle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"help-section"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;How Jotto Differs from Wordle&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Unlike Wordle, Jotto:&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Only tells you HOW MANY letters match, not which specific ones&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Doesn't give information about letter positions&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Requires deduction across multiple guesses to determine which letters are in the word&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also added a detailed example table showing how matches are counted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"help-section"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Example&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Secret word: "TRAIN"&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"help-example-table"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Guess&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Matching Letters&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Explanation&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;PLANE&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;A, N, E are in TRAIN&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;STORM&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;R is in TRAIN&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;BRAIN&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;4&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;R, A, I, N are in TRAIN&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;TRAIN&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;5&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;All letters match - you win!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These improvements collectively make the Jotto mechanics much clearer to players, especially those who might be more familiar with Wordle. By emphasizing the deduction aspect and providing clear guidance, we've made the game more accessible while maintaining its classic gameplay.&lt;/p&gt;

&lt;h1&gt;
  
  
  AmazonQDevCLI #BuildGamesChallenge #AmazonQCLI
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Check out this post on one of the non-functional requirements for your applications - Security</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Wed, 08 Jan 2025 21:30:00 +0000</pubDate>
      <link>https://dev.to/cloudsege/check-out-this-post-on-one-of-the-non-functional-requirements-for-your-applications-security-i88</link>
      <guid>https://dev.to/cloudsege/check-out-this-post-on-one-of-the-non-functional-requirements-for-your-applications-security-i88</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/aws-builders" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2794%2F88da75b6-aadd-4ea1-8083-ae2dfca8be94.png" alt="AWS Community Builders " width="350" height="350"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1037830%2F5768f8f1-81ca-4d33-bcd1-0d9312b2451c.jpg" alt="" width="400" height="400"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/aws-builders/non-functional-application-requirements-security-4613" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Non-functional Application Requirements: Security&lt;/h2&gt;
      &lt;h3&gt;Oluwasegun Adedigba for AWS Community Builders  ・ Jan 7&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#aws&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#security&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#discuss&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#softwaredevelopment&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>security</category>
      <category>aws</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Non-functional Application Requirements: Security</title>
      <dc:creator>Oluwasegun Adedigba</dc:creator>
      <pubDate>Tue, 07 Jan 2025 23:30:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/non-functional-application-requirements-security-4613</link>
      <guid>https://dev.to/aws-builders/non-functional-application-requirements-security-4613</guid>
      <description>&lt;p&gt;When you’re building modern applications, security is paramount. It’s the invisible architecture that keeps user data safe, systems protected, and your organization’s reputation intact. In an era where cyber threats are not just probable but inevitable, overlooking security can have catastrophic results.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So how do you approach security in a meaningful, sustainable way?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article dives into securing applications on the cloud, with a focus on leveraging AWS’s rich security toolkit. We’ll address the &lt;strong&gt;5Ws (What, Why, When, Where, and Who)&lt;/strong&gt; of security, explore the vital aspects of a robust security strategy, and discuss best practices that seasoned engineers employ to keep applications safe and resilient. &lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;What: Understanding Security’s Scope in Cloud Applications&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Security in the cloud isn’t just about blocking bad actors. It’s about creating a secure, resilient environment that preserves your application's &lt;strong&gt;confidentiality, integrity, and availability (CIA)&lt;/strong&gt;. Let’s break down these core components: &lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;1. Confidentiality&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Ensuring data is accessible only to authorized users and systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Trust Architecture:&lt;/strong&gt; Enforces strict identity verification at every access point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption Standards:&lt;/strong&gt; Protect data with AES-256 encryption at rest and TLS 1.3 during transmission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Management:&lt;/strong&gt; Utilize systems like AWS Key Management Service (KMS) for secure encryption key storage and rotation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;a href="https://www.bluefin.com/bluefin-news/biggest-data-breaches-year-2024/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;2024, AT&amp;amp;T experienced a significant data breach affecting approximately 73 million customers&lt;/a&gt;, including both current and former users. Sensitive information such as social security numbers, account numbers, and passcodes was compromised. The data, believed to be from 2019 or earlier, surfaced on the dark web in mid-March 2024. This incident underscores the critical importance of robust encryption and stringent access controls to maintain confidentiality. &lt;/p&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;2. Integrity&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Assuring the accuracy and completeness of data throughout its lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic Hash Functions:&lt;/strong&gt; Use SHA-256 or SHA-3 to validate data integrity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutable Audit Logs:&lt;/strong&gt; Maintain unalterable event logs for forensic investigations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control Systems:&lt;/strong&gt; Employ tools like Git to track and revert unauthorized changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2024, &lt;a href="https://www.wired.com/story/worst-hacks-2024?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Snowflake, a prominent data warehousing company, suffered a breach&lt;/a&gt;, in which hackers used stolen passwords to access data from companies like Ticketmaster, Santander Bank, and AT&amp;amp;T. This breach resulted in significant data loss and highlighted the necessity for robust data validation mechanisms and stringent access controls to ensure data integrity. &lt;/p&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;3. Availability&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Ensuring systems remain operational and accessible during critical times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Region Redundancy:&lt;/strong&gt; Host systems across multiple regions to ensure resilience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDoS Protection:&lt;/strong&gt; Utilize services like AWS Shield to mitigate attacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2024, &lt;a href="https://www.politico.com/news/2024/12/27/chinese-hackers-telco-access-00196082?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Chinese hackers, identified as Salt Typhoon, infiltrated U.S. telecommunications networks&lt;/a&gt;, enabling them to geolocate millions of Americans and record their phone calls. High-profile victims included President-elect Donald Trump and senior Biden administration officials. This breach, affecting major telecom providers like Verizon and AT&amp;amp;T, underscores the importance of robust security measures to maintain system availability and protect against unauthorized access. &lt;/p&gt;

&lt;h2&gt;
  
  
  Together, the CIA triad forms the backbone of a secure cloud application and dictates how security should be implemented across various layers.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Security Domains in Cloud Applications&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;1. Identity and Access Management (IAM)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Securing who can access your systems and what actions they can perform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication &amp;amp; Authorization:&lt;/strong&gt; AWS IAM and Cognito provide robust tools for user authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privileged Access Management:&lt;/strong&gt; Enforce least-privilege principles for sensitive operations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;2. Data Security&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Safeguarding data through its entire lifecycle is critical for compliance and user trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encryption Standards:&lt;/strong&gt; Apply AES-256 and TLS 1.3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Retention Policies:&lt;/strong&gt; Regularly dispose of unnecessary data to minimize exposure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last year, &lt;a href="https://therecord.media/meta-fined-263-million-gdpr-violations-data-breach?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Meta (formerly Facebook) was fined €1.2 billion ($1.3 billion)&lt;/a&gt; for improperly transferring Facebook users’ personal data from the European Union to the U.S., violating GDPR. This significant penalty underscores the necessity for robust data security measures and compliance with international data protection laws. &lt;/p&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;3. Application Security&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Securing codebases and runtime environments is key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure SDLC:&lt;/strong&gt; Integrate security in development workflows with automated scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Protection:&lt;/strong&gt; Use tools like AWS AppRunner for secure application deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Key Areas of Security Focus&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;With the transition to cloud-native architectures, applications now require a multi-layered approach that spans various domains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Identity and Access Management (IAM):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This domain centers on authenticating who can access your systems (authentication) and determining what actions they can perform (authorization).&lt;/li&gt;
&lt;li&gt;Key tools include &lt;strong&gt;AWS Identity and Access Management (IAM)&lt;/strong&gt;, &lt;strong&gt;privileged access management&lt;/strong&gt;, and &lt;strong&gt;single sign-on (SSO) integration&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt; Implement strict &lt;strong&gt;least privilege&lt;/strong&gt; policies, use multi-factor authentication (MFA), and enforce short-lived access tokens for sensitive operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protecting data, whether at rest or in transit, is crucial for compliance and user trust.&lt;/li&gt;
&lt;li&gt;Key strategies include &lt;strong&gt;encryption standards and protocols&lt;/strong&gt; (such as AES-256 for at-rest data and TLS 1.3 for in-transit), &lt;strong&gt;regular key rotation policies&lt;/strong&gt;, and &lt;strong&gt;data retention and disposal practices&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Compliance:&lt;/strong&gt; Adhere to regulations like &lt;strong&gt;GDPR&lt;/strong&gt; and &lt;strong&gt;CCPA&lt;/strong&gt; by incorporating privacy-by-design principles into data storage and processing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Network Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Establishing a secure network perimeter is vital for preventing unauthorized access and segmentation of sensitive resources.&lt;/li&gt;
&lt;li&gt;Core tools include &lt;strong&gt;microsegmentation&lt;/strong&gt;, &lt;strong&gt;Web Application Firewalls (WAF)&lt;/strong&gt;, &lt;strong&gt;API gateways&lt;/strong&gt;, and &lt;strong&gt;Virtual Private Cloud (VPC) design with network isolation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt; Limit access through security groups, use VPC endpoints for private connections, and implement network monitoring for anomaly detection.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Application Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application security focuses on securing the codebase and runtime environment.&lt;/li&gt;
&lt;li&gt;Techniques include &lt;strong&gt;secure software development lifecycle (SDLC) practices&lt;/strong&gt;, &lt;strong&gt;dependency scanning&lt;/strong&gt; for vulnerabilities, &lt;strong&gt;container security&lt;/strong&gt;, and &lt;strong&gt;runtime protection&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt; Emphasize secure coding standards, use automated vulnerability scanning, and implement role-based access control within your development pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Operational Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operational security is about maintaining security in day-to-day operations, including monitoring, incident response, and threat modeling.&lt;/li&gt;
&lt;li&gt;Key elements include &lt;strong&gt;continuous monitoring&lt;/strong&gt;, &lt;strong&gt;incident response planning&lt;/strong&gt;, &lt;strong&gt;threat modeling&lt;/strong&gt;, and &lt;strong&gt;security automation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt; Integrate monitoring tools like &lt;strong&gt;AWS GuardDuty&lt;/strong&gt; and &lt;strong&gt;AWS CloudTrail&lt;/strong&gt; for real-time insights, conduct regular security drills, and automate incident response with predefined playbooks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key to a comprehensive security strategy is a multi-layered approach—one that addresses each of these areas, embedding security into the application's entire lifecycle.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Why: The Critical Importance of Security&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Security isn’t just a “nice-to-have” feature. It’s essential. Let’s consider why a robust security strategy is non-negotiable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Financial Impact:&lt;/strong&gt; The average cost of a data breach is estimated to be &lt;strong&gt;over $4.88 million globally&lt;/strong&gt;—a 10% increase over last year and the highest total ever, according to &lt;a href="https://www.ibm.com/reports/data-breach" rel="noopener noreferrer"&gt;IBM’s 2024 Cost of a Data Breach Report&lt;/a&gt;. This cost includes data recovery, legal fees, loss of business, and more. These costs can be even higher for companies that handle sensitive information, like healthcare or financial data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust and Reputation:&lt;/strong&gt; A single data breach can erode user trust for years. The reputational damage can lead to customer churn, media fallout, and loss of market standing—consequences that can be difficult, if not impossible, to recover from.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compliance Requirements:&lt;/strong&gt; With the rise of global data privacy regulations like &lt;strong&gt;GDPR&lt;/strong&gt;, &lt;strong&gt;CCPA&lt;/strong&gt;, and &lt;strong&gt;HIPAA&lt;/strong&gt;, companies face strict standards around data handling, storage, and access. Non-compliance can result in heavy fines and sanctions, making security an essential part of business continuity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operational Stability:&lt;/strong&gt; Insecure applications are more vulnerable to cyber attacks, which can lead to downtime, data loss, and operational disruption. Security ensures your application remains stable, even in the face of unforeseen events.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When security is compromised, it’s not just a technical issue—it’s a business crisis. Prioritizing security helps safeguard your organization’s financial health, reputation, and operational resilience.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;When: Embedding Security from Day One&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In today’s rapid development environments, security needs to be a part of the &lt;strong&gt;design and development process&lt;/strong&gt; from the outset. The earlier security is embedded, the less costly it is to manage and mitigate threats later on. This shift-left approach to security focuses on &lt;strong&gt;integrating security practices throughout the entire development lifecycle&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Key Stages to Integrate Security&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design Phase:&lt;/strong&gt; Consider security requirements and outline data flow models to anticipate potential vulnerabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Phase:&lt;/strong&gt; Emphasize secure coding practices, such as avoiding hard-coded secrets, using secure libraries, and adopting code review processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and QA Phase:&lt;/strong&gt; Use penetration testing and automated vulnerability scanning to identify weaknesses before deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment and Operations Phase:&lt;/strong&gt; Implement real-time monitoring, automated patching, and incident response plans for continuous security.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By shifting security left, you address potential issues early, reducing the risk of costly fixes and rework down the road. &lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Where: Leveraging AWS Security Services&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;AWS offers a broad array of services that cover different aspects of application security. Let’s explore some of the key tools and how they align with specific security needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AWS Identity and Access Management (IAM):&lt;/strong&gt; Centralized access control that enables granular permissions and the principle of least privilege.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Cognito:&lt;/strong&gt; Provides secure, scalable user authentication and authorization, which is ideal for applications with user sign-ups and logins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Key Management Service (KMS):&lt;/strong&gt; Facilitates data encryption at rest and ensures sensitive information is only accessible to authorized users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Shield and AWS Web Application Firewall (WAF):&lt;/strong&gt; Protects against DDoS attacks and filters incoming traffic to block malicious activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon GuardDuty and AWS CloudTrail:&lt;/strong&gt; Enables threat detection and continuous monitoring, providing insights into suspicious activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Config and AWS Inspector:&lt;/strong&gt; Ensures compliance by auditing configurations and automatically scanning for vulnerabilities.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AWS makes it possible to implement a &lt;strong&gt;multi-layered security approach&lt;/strong&gt; tailored to your specific needs, whether you’re building a simple application or a complex, distributed architecture. &lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Who: The Roles and Responsibilities in Security&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Security is a shared responsibility, not just within your organization but also with your cloud provider. In AWS’s &lt;strong&gt;Shared Responsibility Model&lt;/strong&gt;, AWS manages the security of the cloud infrastructure, while you are responsible for securing what you deploy on it.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Roles Within Your Organization&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Developers:&lt;/strong&gt; Responsible for secure coding, dependency management, and building secure APIs. Developers ensure that code meets security standards before it reaches production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Architects and DevOps Engineers:&lt;/strong&gt; Focused on infrastructure security, automation of security tasks, and managing access and identity controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Analysts:&lt;/strong&gt; Monitor security alerts, conduct risk assessments, and respond to incidents. They implement and oversee security policies across the application lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance and Legal Teams:&lt;/strong&gt; Ensure the application meets regulatory and data protection standards, working closely with technical teams to interpret compliance requirements.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By fostering a security-minded culture and involving each role, you create a more resilient, secure application environment. &lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Security Best Practices: Building a Resilient Application&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Embrace the Principle of Least Privilege:&lt;/strong&gt; Only grant access permissions that are absolutely necessary, and regularly audit permissions to prevent “scope creep.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt Everything:&lt;/strong&gt; Use AWS KMS to encrypt data at rest and HTTPS for data in transit. Encrypting sensitive data helps prevent unauthorized access, even in the event of a data breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regularly Patch and Update:&lt;/strong&gt; Outdated software and libraries can be security vulnerabilities. Automate patching to keep your stack up to date and minimize security risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement Multi-Factor Authentication (MFA):&lt;/strong&gt; Enforce MFA for both root and user accounts, adding an additional layer of protection against unauthorized access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuously Monitor and Respond:&lt;/strong&gt; Use AWS CloudTrail and GuardDuty to detect unusual behavior, and implement automated alerts for faster incident response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit for Compliance:&lt;/strong&gt; Regularly assess your application against regulatory requirements. AWS Config and AWS Artifact can simplify auditing and reporting processes, making it easier to stay compliant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conduct Regular Security Reviews:&lt;/strong&gt; Schedule periodic penetration testing and security assessments to identify vulnerabilities that automated tools might miss.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A security-first approach isn’t just about prevention; it’s about preparation. The best security strategies ensure that if an incident occurs, it’s managed swiftly and effectively.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;A Security-First Mindset&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In today’s world, security is a differentiator. Applications that prioritize security not only protect data and maintain compliance, but they also build trust. Users are more likely to stay loyal to a product they trust with their information.&lt;/p&gt;

&lt;p&gt;Adopting a security-first mindset involves viewing security as an enabler rather than an obstacle. By following best practices, leveraging AWS’s rich suite of security tools, and embedding security at every stage of development, you can create applications that are as secure as they are functional. &lt;/p&gt;

&lt;p&gt;Building applications in the cloud has made it easier to scale and innovate, but it has also introduced new security challenges. With AWS’s powerful security tools and a clear, robust security strategy, you can meet these challenges head-on. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember: A secure application isn’t just a technical asset; it’s a valuable part of your brand. Make security a priority, and build trust one secure interaction at a time.&lt;/strong&gt; &lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;strong&gt;Cover Image:&lt;/strong&gt; &lt;a href="https://youtu.be/UTRBVPvzt9w?t=994" rel="noopener noreferrer"&gt;AWS re:Invent 2023 - Keynote with Dr. Werner Vogels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>discuss</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
