<?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>Forem: Avinash Dalvi</title>
    <description>The latest articles on Forem by Avinash Dalvi (@avinashdalvi_).</description>
    <link>https://forem.com/avinashdalvi_</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F405793%2F84bd1a1e-63ae-4468-bf12-400348bafa19.jpeg</url>
      <title>Forem: Avinash Dalvi</title>
      <link>https://forem.com/avinashdalvi_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/avinashdalvi_"/>
    <language>en</language>
    <item>
      <title>MyRedBuddy: Helping Redditors Contribute Without the Fear of Bans</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Fri, 27 Feb 2026 10:00:31 +0000</pubDate>
      <link>https://forem.com/avinashdalvi_/myredbuddy-helping-redditors-contribute-without-the-fear-of-bans-36p</link>
      <guid>https://forem.com/avinashdalvi_/myredbuddy-helping-redditors-contribute-without-the-fear-of-bans-36p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;MyRedBuddy&lt;/strong&gt; for the &lt;strong&gt;Reddit Community&lt;/strong&gt;, specifically for new contributors, researchers, and indie hackers. &lt;/p&gt;

&lt;p&gt;Reddit is an incredible place for knowledge, but for many, it can be intimidating. High entry barriers like "karma requirements" and strict, often invisible subreddit rules lead to many genuine users getting their posts removed or their accounts banned. I wanted to build a tool that acts as a "buddy" to help users understand how to contribute value without breaking community norms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MyRedBuddy&lt;/strong&gt; is an open-source companion tool designed to help users contribute to Reddit more effectively. &lt;/p&gt;

&lt;p&gt;Key features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subreddit Insights:&lt;/strong&gt; Analyzes subreddit rules and posting patterns to give users a "safety check" before they post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft Management:&lt;/strong&gt; A clean interface to prepare and refine contributions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ban Prevention:&lt;/strong&gt; Guidance on community-specific etiquette to ensure your voice is heard rather than moderated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement Tracking:&lt;/strong&gt; Helping users see where their contributions are making the most impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://youtu.be/ezgdSMyIdp8" rel="noopener noreferrer"&gt;Check out the Live Demo here&lt;/a&gt;&lt;br&gt;


  &lt;iframe src="https://www.youtube.com/embed/ezgdSMyIdp8"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AvinashDalvi89" rel="noopener noreferrer"&gt;
        AvinashDalvi89
      &lt;/a&gt; / &lt;a href="https://github.com/AvinashDalvi89/myredbuddy-tool" rel="noopener noreferrer"&gt;
        myredbuddy-tool
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Analyze your Reddit engagement and get AI-powered validator to write better posts and comments.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;MyRedBuddy&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Post with confidence. Get engagement, not banned.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MyRedBuddy is an action-oriented Reddit engagement tool that protects genuine users from bans, downvotes, and karma loss while helping them grow authentically.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AvinashDalvi89/myredbuddy-tool/dashboard/public/myredditbuddy-logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FAvinashDalvi89%2Fmyredbuddy-tool%2Fdashboard%2Fpublic%2Fmyredditbuddy-logo.png" alt="MyRedBuddy"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why MyRedBuddy?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Reddit is unforgiving. One wrong post can destroy your karma, get you shadowbanned, or kill your reputation in a community you care about. MyRedBuddy gives you a safety net:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No account required to start&lt;/strong&gt; - Use Community Guide, Pre-Post Checker, and Reputation Shield with zero Reddit history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-post protection&lt;/strong&gt; - Check content BEFORE you post, not after&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn from YOUR data&lt;/strong&gt; - See what works for YOU specifically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action-oriented&lt;/strong&gt; - Every insight has an immediate action button&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% local&lt;/strong&gt; - Your data never leaves your machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subreddit-specific&lt;/strong&gt; - Each community is different&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Needs Account?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community Guide&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Culture briefing for any subreddit — vibe, unwritten rules, what gets removed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pre-Post Checker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Validate a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AvinashDalvi89/myredbuddy-tool" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;As a Product Engineer, I focused on building something functional and user-centric over this weekend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; Next.js for a fast, SEO-friendly terminal-style interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reddit Integration:&lt;/strong&gt; Leveraging the Reddit API to fetch real-time community guidelines and activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Assistance:&lt;/strong&gt; Used AI to help analyze complex subreddit wikis and summarize them into actionable "Dos and Don'ts."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS for a clean, minimalist "developer-first&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
      <category>reddit</category>
    </item>
    <item>
      <title>How I Replaced Prerender.io with My Own Serverless Renderer on AWS — For $0/Month</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Fri, 27 Feb 2026 06:01:09 +0000</pubDate>
      <link>https://forem.com/aws-builders/how-i-replaced-prerenderio-with-my-own-serverless-renderer-on-aws-for-0month-344c</link>
      <guid>https://forem.com/aws-builders/how-i-replaced-prerenderio-with-my-own-serverless-renderer-on-aws-for-0month-344c</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;The Problem That Started It All&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A few months ago I published a post about using &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; with Angular (&lt;a href="https://www.internetkatta.com/how-i-fixed-seo-for-our-angular-spa-using-aws-amplify-prerenderio" rel="noopener noreferrer"&gt;&lt;strong&gt;https://www.internetkatta.com/how-i-fixed-seo-for-our-angular-spa-using-aws-amplify-prerenderio&lt;/strong&gt;&lt;/a&gt;). The approach worked, but when I checked my bill I was paying ₹5,000/month ( $49) to &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; for essentially zero usage.&lt;/p&gt;

&lt;p&gt;My app is an Angular SPA hosted on AWS Amplify. Angular renders everything client-side using JavaScript. Social bots like WhatsApp, LinkedIn, Googlebot, and Telegram don't execute JavaScript. They crawl your URL, get a blank HTML shell, and your link preview shows nothing. No title. No image. No description.&lt;/p&gt;

&lt;p&gt;Prerender.io solves this by running a headless browser on their servers, rendering your page, and returning the fully-rendered HTML to bots. It works well. But at ₹5,000/month, I was paying for a service that was essentially idling — my platform was still early stage, getting very little traffic while I worked on getting traction.&lt;/p&gt;

&lt;p&gt;That's ₹5,000/month for almost zero usage. No scaling. No pay-per-use. Just a flat fee.&lt;/p&gt;

&lt;p&gt;I started asking: can I build this myself on AWS and pay only for what I actually use?&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Understanding the Existing Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before building anything, I needed to understand exactly what prerender.io was doing for me. The architecture flow was: Regular users bypass all of this entirely and hit Amplify directly. The key insight: &lt;strong&gt;prerender.io was just a CloudFront origin&lt;/strong&gt;. The Lambda@Edge was doing the bot detection and routing. prerender.io itself was a black box sitting at the end of that route. If I could replace that black box with my own renderer, I wouldn't need to touch the bot detection logic at all.&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%2Fnloa0yy711vnwjkyc7h0.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%2Fnloa0yy711vnwjkyc7h0.png" alt=" " width="437" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Designing the Replacement&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;So, the requirements were clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Serverless — pay only when a bot actually hits a page&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No fixed monthly cost&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Same output as prerender.io — fully rendered HTML&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No changes to the Angular app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Minimal changes to the bot detection logic in Lambda@&lt;a href="https://dev.to@david264"&gt;EDGE&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core idea was to replace this: Lambda@Edge origin-request → &lt;a href="http://service.prerender.io" rel="noopener noreferrer"&gt;service.prerender.io&lt;/a&gt; to new flow &lt;strong&gt;Puppeteer&lt;/strong&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%2Fed9pob66oim9x7hwud9l.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%2Fed9pob66oim9x7hwud9l.png" width="800" height="705"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why Puppeteer +&lt;/strong&gt; &lt;code&gt;networkidle0&lt;/code&gt;&lt;strong&gt;?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Prerender.io works without any changes to the Angular app. It uses a headless Chrome browser that waits until the page has no network activity for 500ms (&lt;code&gt;networkidle0&lt;/code&gt;). This gives Angular enough time to finish fetching data and rendering the DOM. The same approach works in our own Lambda — no Angular code changes needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why S3 for Caching?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A rendered page doesn't change every second. An article published today will have the same meta tags tomorrow. Caching the rendered HTML in S3 means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First bot request for a URL: Puppeteer renders it (5–10 seconds, acceptable for bots)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every subsequent bot request: S3 returns it in ~300ms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cache TTL: 24 hours (configurable)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;This is how architecture look like.&lt;/strong&gt;
&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%2Fshgnl3h3wkn5wi80vkf3.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%2Fshgnl3h3wkn5wi80vkf3.png" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Code&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lambda@Edge —&lt;/strong&gt; &lt;code&gt;socialbots&lt;/code&gt; &lt;strong&gt;function&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This runs at CloudFront edge. The key change from the original prerender.io version is the last 5 lines of the origin-request block. Bot detection logic is untouched.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&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;INTERNAL_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-secret-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// same value as renderer Lambda INTERNAL_TOKEN env var&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&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;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-host&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;// ── ORIGIN-REQUEST: bot detected in viewer-request, now route to renderer ──&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-query-string&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-query-string&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// CRITICAL: When Lambda@Edge changes request.origin, CloudFront does NOT&lt;/span&gt;
        &lt;span class="c1"&gt;// automatically update the Host header. API Gateway rejects requests where&lt;/span&gt;
        &lt;span class="c1"&gt;// Host doesn't match its configured custom domain → ForbiddenException.&lt;/span&gt;
        &lt;span class="c1"&gt;// Must set Host explicitly before setting request.origin.&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;precache.myapp.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;precache.myapp.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ← was: service.prerender.io&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// ← was: 20 (Puppeteer needs up to 25s)&lt;/span&gt;
                &lt;span class="na"&gt;keepaliveTimeout&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="na"&gt;customHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-token&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;// auth token sent to renderer&lt;/span&gt;
                        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INTERNAL_TOKEN&lt;/span&gt;
                    &lt;span class="p"&gt;}]&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;sslProtocols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TLSv1.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                 &lt;span class="c1"&gt;// ← was: TLSv1, TLSv1.1 (deprecated)&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;                                   &lt;span class="c1"&gt;// ← was: '/https%3A%2F%2F' + host&lt;/span&gt;
            &lt;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="c1"&gt;// ── VIEWER-REQUEST: detect bots, set headers ── (completely unchanged)&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;user_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&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;user_agent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/googlebot|adsbot&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;google|Feedfetcher&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;Google|bingbot|yandex&lt;/span&gt;&lt;span class="err"&gt;|
&lt;/span&gt;                &lt;span class="nx"&gt;baiduspider&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;Facebot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;facebookexternalhit&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;twitterbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;rogerbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;linkedinbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;embedly&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;quora&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;showyoubot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;outbrain&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;pinterest&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;slackbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;vkShare&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;W3C_Validator&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;redditbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;applebot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;whatsapp&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;flipboard&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;tumblr&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;bitlybot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;skypeuripreview&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;nuzzel&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;discordbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;qwantify&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;pinterestbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;bitrix&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;xing&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;contenttabreceiver&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;lighthouse&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;telegrambot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;Perplexity&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;OAI&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;SearchBot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;ChatGPT&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;GPTBot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;ClaudeBot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;Amazonbot&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;user_agent&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/_escaped_fragment_/&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prerender&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt&lt;/span&gt;&lt;span class="err"&gt;|
&lt;/span&gt;                &lt;span class="nx"&gt;ico&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;mp3&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;rar&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;exe&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;wmv&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;avi&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;ppt&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;mpg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;mpeg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;tif&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;wav&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;mov&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;psd&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;xls&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="nx"&gt;mp4&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;m4a&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;swf&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;dat&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;dmg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;iso&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;m4v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;torrent&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;ttf&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;woff&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;eot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&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;prerender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bot detected:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user_agent&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INTERNAL_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-host&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;host&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;value&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-cachebuster&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Cachebuster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-query-string&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Query-String&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt; &lt;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;callback&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="nx"&gt;request&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Deploy note&lt;/strong&gt;: Lambda@Edge must be in &lt;code&gt;us-east-1&lt;/code&gt;. After publishing a new version, update both the viewer-request and origin-request ARNs in your CloudFront behaviour to point to the new version number (e.g. &lt;code&gt;:12&lt;/code&gt; → &lt;code&gt;:13&lt;/code&gt;). CloudFront takes 5–10 minutes to propagate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Renderer Lambda —&lt;/strong&gt; &lt;code&gt;index.js&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This runs in &lt;code&gt;ap-south-1&lt;/code&gt; ( I choose Mumbai because my app run on this region) as a container image. Receives bot requests from API Gateway, checks S3 cache, renders with Puppeteer if needed.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&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;chromium&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sparticuz/chromium&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;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer-core&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;S3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutObjectCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-s3&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;s3&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;S3Client&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;BUCKET&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHE_BUCKET&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;CACHE_TTL_MS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHE_TTL_HOURS&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;24&lt;/span&gt;&lt;span class="dl"&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;3600&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;INTERNAL_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_TOKEN&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;SITE_URL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://myapp.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Reuse browser across warm Lambda invocations — saves 3-5s Chromium startup time&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;browser&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBrowser&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;browser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connected&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;browser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;defaultViewport&lt;/span&gt;&lt;span class="p"&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="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;executablePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executablePath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;headless&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pathToS3Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlPath&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;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urlPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;+|&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;+$/g&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="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index&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="s2"&gt;`cache/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Examples:&lt;/span&gt;
    &lt;span class="c1"&gt;//   /article/my-slug  →  cache/article/my-slug.html&lt;/span&gt;
    &lt;span class="c1"&gt;//   /                 →  cache/index.html&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

    &lt;span class="c1"&gt;// Reject requests without the internal token&lt;/span&gt;
    &lt;span class="c1"&gt;// (prevents anyone who discovers the URL from triggering renders)&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;INTERNAL_TOKEN&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-internal-token&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;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;INTERNAL_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rejected: wrong or missing token&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="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&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;urlPath&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawPath&lt;/span&gt; &lt;span class="o"&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="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&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;targetUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;urlPath&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3Key&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pathToS3Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ── 1. Check S3 cache &lt;/span&gt;
    &lt;span class="k"&gt;try&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;cached&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Bucket&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="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s3Key&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;renderedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rendered-at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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="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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;renderedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL_MS&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transformToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`CACHE HIT [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;urlPath&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="na"&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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&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;text/html; charset=utf-8&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;x-prerender-cache&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;HIT&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`CACHE STALE [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;urlPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] — re-rendering`&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="nx"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NoSuchKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S3 read error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`CACHE MISS [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;urlPath&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="c1"&gt;// ── 2. Render with Puppeteer ──────────────────────────────────────────────&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rendering: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;html&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBrowser&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Block images, fonts, media — bots only need HTML + meta tags.&lt;/span&gt;
        &lt;span class="c1"&gt;// Blocking these cuts render time by 30-60%.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestInterception&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;image&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;font&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;media&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resourceType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;req&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// networkidle0: wait until no network activity for 500ms.&lt;/span&gt;
        &lt;span class="c1"&gt;// This is how prerender.io works — Angular finishes data fetching and rendering.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Render failed [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;browser&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nx"&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;browser&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="c1"&gt;// force fresh browser on next invocation&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="na"&gt;statusCode&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Render error&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;// ── 3. Store in S3 cache ──────────────────────────────────────────────────&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;await&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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;PutObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;Bucket&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="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s3Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rendered-at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Cached → &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s3Key&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S3 write error (non-fatal):&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="na"&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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&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;text/html; charset=utf-8&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;x-prerender-cache&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;MISS&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;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;
  
  
  &lt;strong&gt;Dockerfile&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# AWS Lambda Node.js 20 base image (Amazon Linux 2023)&lt;/span&gt;
&lt;span class="c"&gt;# @sparticuz/chromium v133+ is compatible with AL2023&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; public.ecr.aws/lambda/nodejs:20&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; index.js ./&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["index.handler"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;package.json&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prerender-renderer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@aws-sdk/client-s3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.741.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@sparticuz/chromium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^133.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"puppeteer-core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^24.0.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build note&lt;/strong&gt;: Always build with &lt;code&gt;--platform linux/amd64 --provenance=false&lt;/code&gt; on Mac. The &lt;code&gt;--provenance=false&lt;/code&gt; flag prevents Docker Desktop from creating an OCI manifest list, which Lambda doesn't support.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;--provenance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; renderer &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Problems We Hit Along the Way&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Problem 1: Lambda Block Public Access (Account-Level)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The renderer Lambda needed an HTTP endpoint CloudFront could call as a custom origin. The natural choice was &lt;strong&gt;Lambda Function URL&lt;/strong&gt; — no extra services, free, simple.&lt;/p&gt;

&lt;p&gt;It returned 403 immediately.&lt;/p&gt;

&lt;p&gt;AWS silently enabled &lt;strong&gt;Lambda Block Public Access&lt;/strong&gt; at the account level in late 2024 (similar to S3's public access block). This blocks all Lambda Function URLs from public internet access, even with &lt;code&gt;AuthType=NONE&lt;/code&gt;. The feature exists for good reasons but wasn't clearly communicated as a default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Use &lt;strong&gt;API Gateway HTTP API&lt;/strong&gt; instead. Same effective cost (&amp;lt; $1/month at any realistic scale for this use case), no public access restrictions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Problem 2: The Host Header&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This was the hardest bug to diagnose. Symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Direct request to &lt;code&gt;precache.myapp.com&lt;/code&gt; with token → 200 ✓&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bot request through CloudFront → 403 from API Gateway&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Response had &lt;code&gt;x-amzn-errortype: ForbiddenException&lt;/code&gt; and &lt;code&gt;content-length: 0&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;content-length: 0&lt;/code&gt; was the clue. Our Lambda's 403 returns body &lt;code&gt;"Forbidden"&lt;/code&gt; (8 bytes). Zero content means the request &lt;strong&gt;never reached our Lambda&lt;/strong&gt; — API Gateway itself was rejecting it.&lt;/p&gt;

&lt;p&gt;Root cause: when Lambda@Edge dynamically changes &lt;code&gt;request.origin&lt;/code&gt;, &lt;strong&gt;CloudFront does not update the&lt;/strong&gt; &lt;code&gt;Host&lt;/code&gt; &lt;strong&gt;header&lt;/strong&gt; to match the new origin domain. The request arrives at API Gateway with &lt;code&gt;Host: myapp.com&lt;/code&gt; instead of &lt;code&gt;Host: precache.myapp.com&lt;/code&gt;. API Gateway rejects it because that host isn't mapped to any API.&lt;/p&gt;

&lt;p&gt;Confirmed with:&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;# Simulates what CloudFront sends without the fix&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Host: myapp.com"&lt;/span&gt; https://YOUR_API_ID.execute-api.ap-south-1.amazonaws.com/
&lt;span class="c"&gt;# → 403 ForbiddenException&lt;/span&gt;

&lt;span class="c"&gt;# Correct Host&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Host: precache.myapp.com"&lt;/span&gt; https://YOUR_API_ID.execute-api.ap-south-1.amazonaws.com/
&lt;span class="c"&gt;# → 200 OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: One line in Lambda@Edge, before setting &lt;code&gt;request.origin&lt;/code&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;precache.myapp.com&lt;/span&gt;&lt;span class="dl"&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 is not documented prominently in AWS guides but is a known gotcha with Lambda@Edge + API Gateway custom domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Scale Analysis and Cost Comparison&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Renders vs Requests — The Critical Distinction&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;prerender.io&lt;/strong&gt; charges per &lt;strong&gt;render&lt;/strong&gt; — a render happens only when their headless Chrome actually runs (cache miss on their end). Repeated requests for the same URL within the cache window don't cost extra renders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our system&lt;/strong&gt; works the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache miss&lt;/strong&gt; = Puppeteer runs → slow (~8s), costs compute&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache hit&lt;/strong&gt; = S3 returns cached HTML → fast (~300ms), costs almost nothing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Our cache is bounded by content, not traffic&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;With 241 content pages and a 24-hour TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum renders per month = 241 pages × 30 days = 7,230

No matter how many millions of requests arrive,
Puppeteer runs at most 7,230 times per month.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 1,000,000 bot requests per month with a 99.3% cache hit rate, we still only render 7,230 times. This is the structural advantage of URL-level S3 caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;AWS Pricing Used (ap-south-1)&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Service&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Lambda invocations&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;First 1M/month free, then $0.20/1M&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Lambda compute&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;First 400,000 GB-s/month free, then $0.0000167/GB-s&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Lambda memory&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;2048MB = 2GB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Cache miss compute&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;2GB × 8s = 16 GB-s per render&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Cache hit compute&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;2GB × 0.5s = 1 GB-s per request&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;API Gateway HTTP API&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$1.00 per million requests&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;S3 GET&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$0.00043 per 1,000 requests&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;S3 PUT&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$0.0054 per 1,000 requests&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Data transfer out&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;First 100GB/month free&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Cost Comparison at Scale&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Bot Requests/mo&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Renders (cache miss)&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Cache Hits&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Lambda Compute&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;API Gateway&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Our Cost&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;prerender.io $49&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~100 (today)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~20&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~80&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;400 GB-s ✓ free&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$0.00&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$49&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;1,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~200&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~800&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;4,000 GB-s ✓ free&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$0.001&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;~$0&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$49&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;10,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~1,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~9,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;25,000 GB-s ✓ free&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$0.01&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;~$0.01&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$49&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;100,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~3,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~97,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;145,000 GB-s ✓ free&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$0.10&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;~$0.10&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$49&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;1,000,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~7,230&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~992,770&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;1.1M GB-s → $11.81&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$1.00&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;~$13&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$199+&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;5,000,000&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~7,230&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~4,992,770&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;5.1M GB-s → $78&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;$5.00&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;~$99&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Enterprise&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;prerender.io $49 plan includes 25,000 renders/month. Extra renders cost $2 per 1,000. Our system never exceeds 7,230 renders/month (bounded by content count), so we'd never hit their overage pricing either.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;In INR (₹83 = $1 approx)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Today        → ₹0       vs ₹5,000/month   → saves ₹5,000/month
1K req/mo    → ₹0       vs ₹5,000/month   → saves ₹5,000/month
10K req/mo   → ₹1       vs ₹5,000/month   → saves ₹4,999/month
100K req/mo  → ₹10      vs ₹5,000/month   → saves ₹4,990/month
1M req/mo    → ₹1,100   vs ₹16,000+/month → saves ₹14,900+/month
5M req/mo    → ₹8,300   vs Enterprise      → saves significantly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;When Does prerender.io Win?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;At extreme scale (10M+ requests/month) and where &lt;strong&gt;geographic rendering&lt;/strong&gt; matters — prerender.io has global PoPs, so renders happen near the requesting bot. Our renderer is in &lt;code&gt;ap-south-1&lt;/code&gt;. For an Indian platform with Indian bots, this is fine. For a global platform, you'd want renderers in multiple regions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Future Optimization: CloudFront-Level Caching&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Currently every bot request invokes our Lambda (even cache hits, just for 0.5s). At 1M+ requests/month this adds up. The fix: enable &lt;strong&gt;CloudFront caching&lt;/strong&gt; on the &lt;code&gt;/prerender/*&lt;/code&gt; behavior with a 24-hour TTL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First bot request for /article/xyz in 24h
    → Lambda invoked → Puppeteer renders → CloudFront caches response

Next 999 bot requests for same URL in same 24h window
    → CloudFront edge serves directly → Lambda never invoked
    → Cost: $0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This collapses 1M Lambda invocations to ~7,230 per month. At that point the 1M/month scenario costs under $1 instead of $13. Worth implementing when you approach that scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Trade-offs and Honest Assessment&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Advantages&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pay-per-use with a hard ceiling&lt;/strong&gt;: Renders are bounded by content count. 241 pages × 30 days = 7,230 renders max regardless of traffic. Costs can't spiral.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full control&lt;/strong&gt;: Cache TTL, bot detection rules, Puppeteer behaviour — all tunable. With prerender.io, you accept their defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No vendor lock-in&lt;/strong&gt;: One day prerender.io could shut down, change pricing, or have an outage. This infrastructure is yours and runs indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transparency&lt;/strong&gt;: CloudWatch logs show exactly which bots crawl which pages, render durations, cache hit ratios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm cache hits are fast&lt;/strong&gt;: ~300ms, comparable to prerender.io's cached responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Honest Limitations&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cold start on cache miss&lt;/strong&gt;: First bot request for a new URL takes 5–10 seconds. Lambda cold start + Chromium launch + Angular data fetching. Bots are patient, but it's not instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You own the Chromium version&lt;/strong&gt;: If &lt;code&gt;@sparticuz/chromium&lt;/code&gt; has a bug or Chrome updates break something, it's your problem. prerender.io handles this silently. Plan to update the package ~quarterly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lambda@Edge timeout risk&lt;/strong&gt;: Lambda@Edge has a hard 30-second origin timeout. Complex pages that take longer than ~25 seconds to render will return a 504. Hasn't happened in practice, but it's a ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No geographic rendering&lt;/strong&gt;: Our renderer Lambda is in &lt;code&gt;ap-south-1&lt;/code&gt;. For a global platform, bots crawling from the US or Europe add ~150–200ms latency to the render. For an Indian platform with Indian bots, this doesn't matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-time engineering cost&lt;/strong&gt;: Setting up this system took a day of work and debugging. prerender.io takes 30 minutes. Factor this in if your time is expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Final Results&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# WhatsApp bot&lt;/span&gt;
HTTP 200 — 2.09s  &lt;span class="o"&gt;(&lt;/span&gt;cache miss on first request — Puppeteer rendered&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Googlebot (second request, cache hit)&lt;/span&gt;
HTTP 200 — 0.30s

&lt;span class="c"&gt;# LinkedIn&lt;/span&gt;
HTTP 200 — 0.37s

&lt;span class="c"&gt;# Regular user → Amplify, not renderer — no change&lt;/span&gt;
HTTP 200 — 0.18s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendered HTML for the article includes the correct title and meta tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My Article Title | My App&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Link previews on WhatsApp, LinkedIn, and Twitter work correctly. Googlebot indexes full content. The ₹5,000/month prerender.io subscription is cancelled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost: ₹0/month today. ₹1,100/month at 1 million bot requests. ₹8,300/month at 5 million.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>angular</category>
      <category>serverless</category>
      <category>amplify</category>
    </item>
    <item>
      <title>My First AWS re:Invent Experience</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Mon, 29 Dec 2025 11:08:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/my-first-aws-reinvent-experience-50ih</link>
      <guid>https://forem.com/aws-builders/my-first-aws-reinvent-experience-50ih</guid>
      <description>&lt;p&gt;Ten years.&lt;/p&gt;

&lt;p&gt;That's how long I'd been waiting to attend re:Invent. Ten years of watching from afar, reading live tweets, consuming session recordings days later, imagining what it would feel like to be there in person.&lt;/p&gt;

&lt;p&gt;This year, AWS launched a grant program for User Group Leaders. After a decade of being part of AWS community, attending, volunteering, contributing and organising meet-ups, answering questions at midnight, showing up week after week and I finally got the grant.&lt;/p&gt;

&lt;p&gt;I was on cloud nine. The long-awaited dream was finally happening.&lt;/p&gt;

&lt;p&gt;But here's what they don't tell you about dreams coming true: they rarely arrive smoothly. There are always ups and downs, tests you didn't prepare for, moments that ask you to prove how badly you really want it.&lt;/p&gt;

&lt;p&gt;Mine came twenty-four hours before takeoff.&lt;/p&gt;

&lt;p&gt;Most re:Invent stories start with excitement—the kind you share in Slack channels and LinkedIn posts. Mine started with a phone screen lighting up in a London airport terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Call That Changed Everything
&lt;/h2&gt;

&lt;p&gt;Twenty-four hours earlier, my wife wasn't feeling well. Still, she looked at me with that determined expression I've come to recognise over the years and said, "You finally got this chance. Go. Don't miss it. I'll handle everything here."&lt;/p&gt;

&lt;p&gt;The weight of that sentence was the trust, the sacrifice, the quiet strength and it doesn't leave you. It becomes part of the journey itself.&lt;/p&gt;

&lt;p&gt;I boarded my first flight trying to convince myself everything would be fine. The nervous energy of a first-time re:Invent attendee mixed with the worry of leaving home when things weren't perfect. But we'd made the decision. I was going.&lt;/p&gt;

&lt;p&gt;Then came the message.&lt;/p&gt;

&lt;p&gt;Waiting for my connection at Heathrow, surrounded by the usual airport chaos, my phone buzzed. The hospital. My son had fallen. Two fractures. One dislocation. His arm.&lt;/p&gt;

&lt;p&gt;I called home immediately. My wife picked up, her voice steady as she walked me through what happened, what the doctors said, what came next. In the background, I could hear the sounds of the emergency room. And as we spoke, trying to process it all, I saw the airline staff closing the aircraft door.&lt;/p&gt;

&lt;p&gt;Ten hours in the air where there was no no network. I was waiting for update but can’t do anything.&lt;br&gt;&lt;br&gt;
Just the hum of engines and one thought playing on repeat: &lt;em&gt;How is she managing all this alone, when she herself isn't well?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That flight became a masterclass in helplessness. In recognising that behind every conference badge, every community contribution, every public achievement, there are people at home carrying half your world, sometimes more and so you can chase the other half.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Your Mind Finally Lands
&lt;/h2&gt;

&lt;p&gt;The moment my plane touched down in Las Vegas, I didn't care about the Strip or the spectacle. I needed that first call home to work. It did. Things were stable. My son was being treated. My wife was managing with the kind of strength that makes you realise you married someone far braver than yourself.&lt;/p&gt;

&lt;p&gt;That's when I finally arrived. That had been in Vegas for hours. But my mind. My presence. My ability to actually &lt;em&gt;be&lt;/em&gt; at re:Invent.&lt;/p&gt;

&lt;p&gt;And from that point forward, everything shifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meeting the People Who Build the Things I Build On
&lt;/h2&gt;

&lt;p&gt;I talk about ECS and Serverless constantly from product builder point of view. The service I return to, the one I recommend, the one I've built my mental models around.&lt;/p&gt;

&lt;p&gt;This week, I got to meet the people who actually build that home.&lt;/p&gt;

&lt;p&gt;I sat in Eric's session on ECS Managed Instances—the kind of talk where you're not just learning features, you're learning &lt;em&gt;intent&lt;/em&gt;. Why this approach? What problem were they really solving? What trade-offs did they consider?&lt;/p&gt;

&lt;p&gt;I heard the ECS Express Mode introduction straight from the product person and engineer who crafted it. Not through blog posts or documentation, but from the humans who debated, prototyped, and shipped it.&lt;/p&gt;

&lt;p&gt;And here's what hit me: you can read docs. I've read AWS docs all year—they've been my reference point for everything. But talking to the people who think about these problems day and night? Who live inside the trade-offs and the edge cases? That changes &lt;em&gt;how&lt;/em&gt; you understand a service, not just &lt;em&gt;what&lt;/em&gt; you know about it.&lt;/p&gt;

&lt;p&gt;We exchanged ideas. We nerded out about container orchestration. We talked about real problems and real solutions.&lt;/p&gt;

&lt;p&gt;For me, that alone justified the entire trip.&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%2Ftm4ixkaxxue5gmf9j7ht.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm4ixkaxxue5gmf9j7ht.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sessions That Stayed With Me
&lt;/h2&gt;

&lt;p&gt;If I'm being honest, I probably covered 20% of expo area it and that's generous.&lt;/p&gt;

&lt;p&gt;Instead, I planted myself in technical sessions. ECS. Fargate. How teams think about scaling at impossible sizes. Every session fed directly into my "AWS for Product Builders" mindset—the lens I use to evaluate whether something works for startups and growing companies, not just enterprises.&lt;/p&gt;

&lt;p&gt;I'll share the specific technical learnings in another post. But the meta-learning is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hearing the intent behind the service is as valuable as learning the service itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you understand &lt;em&gt;why&lt;/em&gt; a team made certain decisions, you make better decisions yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Slack Avatars to Real Conversations
&lt;/h2&gt;

&lt;p&gt;The Community Hub became my anchor during the chaos.&lt;/p&gt;

&lt;p&gt;This was my first re:Invent, and I walked in carrying a kind of shyness I don't usually admit to. The hesitation to start conversations with new people. The imposter syndrome that whispers &lt;em&gt;everyone here knows more than you&lt;/em&gt;. The Hub was full of heroes. Community Builders whose blogs I'd read for years. Leaders whose work I admired from afar. People I'd wanted to meet but never had the chance.&lt;/p&gt;

&lt;p&gt;And I froze.&lt;/p&gt;

&lt;p&gt;I'd see them across the room and think, "I should go say hello." But my feet wouldn't move. One step—that's all it would take. But that one step felt impossible in those moments.&lt;/p&gt;

&lt;p&gt;I missed talking to people I'd dreamed of meeting. I let opportunities slip by.&lt;/p&gt;

&lt;p&gt;But I also pushed myself. Tiny steps. One introduction. One conversation. Then another. And something magical happened.&lt;/p&gt;

&lt;p&gt;I met AWS User Group Leaders I'd known online for five years—people who felt like old friends even though we'd never shared the same room before.&lt;/p&gt;

&lt;p&gt;I encountered new faces who somehow felt instantly familiar—the kind of connection that reminds you why community work matters in the first place.&lt;/p&gt;

&lt;p&gt;One highlight was the User Group meeting Maria organized. UG leaders shared the real problems they faced and the ones that don't make it into polished LinkedIn posts. How they kept their communities engaged when attendance dropped. How they found speakers. How they dealt with burnout while trying to inspire others.&lt;/p&gt;

&lt;p&gt;At the APJC Community Awards, I met a leader from the Philippines who completely shifted my perspective. For them, community isn't just networking or professional development—it's a lifeline. They shared how incredibly difficult it is to get things done there. The lack of resources. The infrastructure challenges. The uphill battle to create opportunities where few exist.&lt;/p&gt;

&lt;p&gt;Yet they keep showing up. They keep building. They keep creating spaces where people can learn, connect, and grow—because for many in their community, these meetups represent access they simply wouldn't have otherwise.&lt;/p&gt;

&lt;p&gt;Listening to their story made me realize how privileged my own challenges are. It reminded me that community work looks different across the world, and the impact it creates can be measured in opportunities that never would have existed.&lt;/p&gt;

&lt;p&gt;But there was another moment—one I didn't expect to witness, and one I'll never forget.&lt;/p&gt;

&lt;p&gt;Jeff Barr. Twenty years of unwavering commitment to the AWS community. Two decades of blog posts, of showing up, of giving back. The room gathered to honor this milestone, and what happened next was pure, unfiltered emotion.&lt;/p&gt;

&lt;p&gt;His son, Stephen, stood up to share another side of Jeff—the father behind the community legend. Stories from childhood. How Jeff balanced being a dad with being the voice of AWS. The late nights writing. The early mornings answering questions. The way he somehow made space for both family and this massive community he'd built.We all watched Jeff cry. Not the polished, composed tears you see at rehearsed events. Real tears. The kind that come when you realise the full weight of what you've built and who stood beside you while you built it.&lt;/p&gt;

&lt;p&gt;The room was silent except for a few sniffles. Goosebumps. That rare moment when everyone present knows they're witnessing something genuine.&lt;/p&gt;

&lt;p&gt;If someone asked me to name one moment from re:Invent that captured what community really means—the sacrifice, the longevity, the human cost, the profound impact—it would be this one.&lt;/p&gt;

&lt;p&gt;Those stories stayed with me long after the session ended.&lt;/p&gt;

&lt;p&gt;Community-building isn't just planning events and posting updates. It's resilience. It's creativity. It's learning to keep showing up even when you're tired, even when you wonder if it matters, even when the metrics don't move as fast as you'd like.&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%2F9a4qqt9mmv3zelpghq91.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%2F9a4qqt9mmv3zelpghq91.png" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Startup Conversation I Needed
&lt;/h2&gt;

&lt;p&gt;At the Startup Amped event, I found myself in the kind of conversations that don't happen at traditional networking sessions.&lt;/p&gt;

&lt;p&gt;Founders talking about the messy parts. The pivots that felt like failures until they weren't. The risks that kept them up at night. The moment they landed their first customer. The second-guessing. The breakthroughs.&lt;/p&gt;

&lt;p&gt;I shared what we're building at NuShift Connect and our mission to reshape health conversations, awareness, and community in India. How we're trying to fill gaps that the traditional healthcare system leaves open.&lt;/p&gt;

&lt;p&gt;These weren't pitches.&lt;br&gt;&lt;br&gt;
They were "we've been there too" conversations.&lt;br&gt;&lt;br&gt;
They were "here's what I learned the hard way" exchanges.&lt;/p&gt;

&lt;p&gt;And then the conversations went deeper.&lt;/p&gt;

&lt;p&gt;People opened up about their health struggles. Family health crises that happened while they were trying to build their startups. The nights they sat in hospital waiting rooms while their pitch decks sat untouched on their laptops. The impossible choice between being present for a sick parent or showing up for an investor meeting.&lt;/p&gt;

&lt;p&gt;When I shared what had happened with my son just hours before my flight, I saw heads nodding around the room. Not with pity—with recognition. These were founders who understood that life doesn't pause for your business plan. That sometimes your greatest test isn't in the market—it's in the hospital corridor.&lt;/p&gt;

&lt;p&gt;That room felt less like a networking event and more like a circle of people who understand what it really costs to build something from nothing while life happens all around you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Truth About re:Invent: You Can't Be Everywhere
&lt;/h2&gt;

&lt;p&gt;Here's something nobody tells you before your first re:Invent: the event will force you to make impossible choices.The Community Builder mixer was happening at the same time as the Startup Amped event. I chose Startup Amped. Which meant I missed connecting with fellow builders in a space specifically designed for us. Did I regret it? In the moment, yes. Absolutely. But here's what I learned: re:Invent isn't about attending everything. It's not about having a perfect schedule or checking every box. It's about choosing what matters most to you right now, in this season of your journey, and showing up fully for that.&lt;/p&gt;

&lt;p&gt;I missed events. I missed conversations. I missed people.&lt;/p&gt;

&lt;p&gt;But what I didn't miss was being present for the choices I did make.&lt;/p&gt;

&lt;p&gt;And sometimes, that's enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Experiences I Never Planned For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Pre-re:Invent Hike&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before re:Invent officially began, there was the hike. Around 40–50 of us gathered, dividing into two teams: one for the medium route, one for the long and tough route. I chose medium. Seemed reasonable after jet lag and hours of travel. Turns out, "medium" was a generous label. The trail was challenging—longer and steeper than any of us expected. Our team actually reached the end later than the "tough route" group, which became a running joke for the rest of the day. But here's what made it memorable: we didn't just hike. We stopped. We breathed. We talked. Someone pulled out food they'd brought from home—snacks from India, treats from different countries. We shared them on the trail like we'd known each other for years. We tried different paths to test which route worked better. A mental exercise wrapped in physical movement. Problem-solving while hiking. Very builder-like, when you think about it. And the strangest part? After jet lag and a transatlantic flight, I didn't feel tired. The opposite, actually. The mountain air, the movement, the conversations and it all felt energising. Within minutes of starting, strangers opened up about their journeys. Career pivots. Burnout stories. The "I almost quit but..." moments that never make it to LinkedIn. That hike wasn't about reaching the summit. It was about connection, genuine, unexpected, and rare. The kind you can only find when you remove the conference badge and just walk alongside other humans trying to figure things 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.amazonaws.com%2Fuploads%2Farticles%2F4kduwa605k4ipk3w44wi.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%2F4kduwa605k4ipk3w44wi.png" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My First-Ever 5K Run&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd signed up for the 5K run weeks earlier. But somewhere in the chaos of re:Invent, I got confused about the day. Was it Thursday or Wednesday? Then a message popped up in our India community group. The run was happening. Right now. My heart sank. I was going to miss it. Another opportunity slipping away. But something in me said: not this time. I rushed out, found the shuttle bus, my mind racing faster than I'd be running. When I finally reached the starting point, the run had already begun. People were already on the course, their figures disappearing into the early morning light and strong cold was slapping on face and ear. I could have turned back. Found an excuse. Told myself I tried. Instead, I joined them mid-run. My first 5K run. Started late. Arrived breathless. Nothing spectacular about my time. But I showed up. Even when it would have been easier not to. A reminder that even in a heavy week, health doesn't wait for perfect timing and neither should we.&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%2Fsc2595cw4c79a2xm239p.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%2Fsc2595cw4c79a2xm239p.png" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Nomination That Meant More Than Winning
&lt;/h2&gt;

&lt;p&gt;Somewhere in the middle of all this, I found out I'd been nominated for the second year in a row for the AWS Community Builder of the Year award.&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%2F3qosjyxunnzd1dqwdi9a.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%2F3qosjyxunnzd1dqwdi9a.png" width="797" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn't win.&lt;/p&gt;

&lt;p&gt;And honestly? That didn't matter.&lt;/p&gt;

&lt;p&gt;Seeing my name there again was enough. Because I know what it took to reach this point. The late nights answering questions in forums. The blog posts written when I was exhausted. The community events organised on weekends.&lt;/p&gt;

&lt;p&gt;More importantly, I know &lt;em&gt;who&lt;/em&gt; stood behind me so I could do any of it.&lt;/p&gt;

&lt;p&gt;The nomination wasn't just about me. It was about everyone who made space for me to contribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Trip Really Taught Me
&lt;/h2&gt;

&lt;p&gt;This wasn't a smooth trip.&lt;br&gt;&lt;br&gt;
It wasn't a relaxed conference experience.&lt;br&gt;&lt;br&gt;
It wasn't the postcard version of re:Invent you see in highlight reels.&lt;/p&gt;

&lt;p&gt;It was real.&lt;br&gt;&lt;br&gt;
It was emotional.&lt;br&gt;&lt;br&gt;
It stretched me in ways I'm still processing.&lt;/p&gt;

&lt;p&gt;And above everything, it crystallised three truths I already knew but needed to feel again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Family makes the journey possible.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Without my wife's and son strength, I wouldn't have made it past the first airport. Every community contribution I make is built on her and his foundation of support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community makes the journey meaningful.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The technical knowledge matters. But the connections, the shared struggles, the moment you realise someone else has fought the same battles—that's what transforms information into wisdom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Curiosity makes the journey worth continuing.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Even exhausted, even worried, even uncertain—asking questions, seeking understanding, wanting to know &lt;em&gt;why&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt;—that's what keeps us moving forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;I'll be sharing more detailed technical learnings soon. The ECS insights. The Fargate patterns. The "AWS for Product Builders" framework I'm developing. The kind of content I'm excited to give back to the community that's given me so much.&lt;/p&gt;

&lt;p&gt;But for now, I'm sitting with gratitude.&lt;/p&gt;

&lt;p&gt;For my wife, who made an impossible choice to let me go.&lt;br&gt;&lt;br&gt;
For my son, who's recovering with the resilience only kids seem to have.&lt;br&gt;&lt;br&gt;
For the people I met in Vegas who reminded me why this work matters.&lt;br&gt;&lt;br&gt;
For the moments that tested me and, in testing me, changed me.&lt;/p&gt;

&lt;p&gt;My first re:Invent wasn't perfect.&lt;/p&gt;

&lt;p&gt;But it was mine.&lt;/p&gt;

&lt;p&gt;And sometimes, that's exactly what you need.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>community</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>How I Fixed SEO for Our Angular SPA Using AWS Amplify + Prerender.io</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Sat, 29 Nov 2025 13:58:03 +0000</pubDate>
      <link>https://forem.com/aws-builders/how-i-fixed-seo-for-our-angular-spa-using-aws-amplify-prerenderio-fgp</link>
      <guid>https://forem.com/aws-builders/how-i-fixed-seo-for-our-angular-spa-using-aws-amplify-prerenderio-fgp</guid>
      <description>&lt;p&gt;I still remember the excitement of October 22nd, 2025. After months of development and anticipation, &lt;a href="https://nushiftconnect.com/" rel="noopener noreferrer"&gt;Nushift Connect&lt;/a&gt; was finally going live. Built with Angular and hosted on AWS Amplify, everything we'd worked so hard on was about to be in the hands of real users. The deployment was smooth. The app was working beautifully. Then I decided to share one of our articles on LinkedIn to celebrate the launch.&lt;/p&gt;

&lt;p&gt;Instead of our beautiful featured image and carefully crafted description, LinkedIn showed... nothing. Just a bland URL. No image. No description. Generic metadata.&lt;/p&gt;

&lt;p&gt;"Did we forget to add the meta tags?"&lt;/p&gt;

&lt;p&gt;We hadn't. They were there—dynamically generated by Angular. The problem? Social media bots don't execute JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Problem
&lt;/h2&gt;

&lt;p&gt;Here's what was happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regular Users:&lt;/strong&gt; Browser loads our Angular app → JavaScript executes → Dynamic meta tags render → Perfect experience&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social Media Bots:&lt;/strong&gt; Bot requests page → Gets bare HTML (no JavaScript execution) → Sees only static &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag → No rich preview&lt;/p&gt;

&lt;p&gt;Facebook's crawler, LinkedIn's bot, Twitter's card validator—none of them waited for our Angular app to bootstrap and set those meta tags. They needed HTML, and they needed it immediately. I was aware of this limitation of Angular but while working on feature and other parts completely forgot main SEO friendliness.&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%2F5953l2u8xbzt3n4c4nre.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5953l2u8xbzt3n4c4nre.jpeg" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Research Rabbit Hole
&lt;/h2&gt;

&lt;p&gt;I spent the next few days exploring every possible solution:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Move to a Different Platform
&lt;/h3&gt;

&lt;p&gt;"Maybe Netlify handles this better?" I thought. They do have prerendering built-in. ECS with server-side rendering was another option which we could run Angular Universal.&lt;/p&gt;

&lt;p&gt;But here's the thing: &lt;strong&gt;AWS Amplify was perfect for everything else&lt;/strong&gt;. The CI/CD pipeline, the preview branches, the authentication integration, the hosting performance—all excellent. Abandoning it felt like throwing the baby out with the bathwater.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Angular Universal (SSR)
&lt;/h3&gt;

&lt;p&gt;The "proper" solution, right? Server-side rendering would solve this elegantly. But it meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Completely restructuring our application architecture&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dealing with window/document undefined errors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managing a Node.js server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Significantly more complexity for deployments&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a relatively simple SPA, this felt like overkill. We needed something lighter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Prerendering Services
&lt;/h3&gt;

&lt;p&gt;This seemed promising. Services like &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; could crawl our application and serve rendered HTML to bots. The architecture would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Regular users → Direct to Amplify (fast!)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Social media bots → Through prerender service → Get fully rendered HTML&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge? Amplify doesn't have built-in prerendering middleware. We'd need to set it up ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Framework: Why &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; Made Sense
&lt;/h2&gt;

&lt;p&gt;Before committing to any solution, I analysed the actual usage patterns and costs:&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Our Traffic Pattern
&lt;/h3&gt;

&lt;p&gt;Let's be realistic about when prerendering actually happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regular users&lt;/strong&gt; browsing the site: 99%+ of traffic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Social media bots&lt;/strong&gt; crawling shared links: &amp;lt;1% of traffic&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: &lt;strong&gt;Prerendering only happens when someone shares a link on social media.&lt;/strong&gt; Not on every page load. Not for every user. Only when LinkedIn, Facebook, or Twitter bots crawl a URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Analysis
&lt;/h3&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%2Fu4tmn3puifd1m6rqr3m8.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%2Fu4tmn3puifd1m6rqr3m8.png" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Math That Convinced Me
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario: 10,000 page views/month&lt;/strong&gt; (9,900 users + 100 bot crawls)&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%2Fqtknpc95o8qajg1mnjv8.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%2Fqtknpc95o8qajg1mnjv8.png" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda@Edge Cost Breakdown
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Request charges: $0.60 per 1M requests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Duration charges: $0.00005001 per GB-second&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Free tier&lt;/strong&gt;: 1M requests/month (covers most small-medium sites)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Our usage:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Viewer-request: 10,000/month (bot detection on all traffic)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Origin-request: 100/month (redirect only bots)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Memory: 128 MB | Execution: ~10ms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monthly cost: ~$0.007&lt;/strong&gt; (essentially free with free tier)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;At scale:&lt;/strong&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%2F6ktn2pw4q95s95699lmg.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%2F6ktn2pw4q95s95699lmg.png" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Winner:&lt;/strong&gt; &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;&lt;strong&gt;Prerender.io&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;+ Lambda@Edge&lt;/strong&gt; - 90% cost savings, 10x faster implementation, zero infrastructure overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Reality Check
&lt;/h3&gt;

&lt;p&gt;I asked myself: "What am I actually trying to solve?"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ Social media previews when links are shared&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ NOT trying to rank #1 on Google for competitive keywords&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ NOT building a content-heavy blog that needs perfect SEO&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ NOT dealing with thousands of bot crawls per day&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a SPA where social sharing matters but isn't the primary traffic driver, prerendering is the pragmatic choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  When NOT to Choose &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To be fair, &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; isn't always the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Heavy SEO focus&lt;/strong&gt;: If organic search is your primary channel, SSR is better&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-heavy sites&lt;/strong&gt;: News sites, blogs with thousands of articles need full SSR&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;High bot traffic&lt;/strong&gt;: If bots are &amp;gt;10% of traffic, costs add up&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time content&lt;/strong&gt;: Stock prices, live scores need instant SSR&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for our use case—a business application where social sharing enhances discoverability—prerendering was perfect.&lt;/p&gt;

&lt;p&gt;The challenge? Amplify doesn't have built-in prerendering middleware. We'd need to set it up ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CloudFront Discovery
&lt;/h2&gt;

&lt;p&gt;Then it clicked: Amplify uses CloudFront under the hood. And CloudFront has Lambda@Edge—functions that can intercept and modify requests at the edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This was our solution.&lt;/strong&gt; We could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Detect social media bots at the CloudFront level&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Route bot traffic through &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep regular user traffic going directly to Amplify&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Best of both worlds: stay on Amplify, solve the bot problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: CloudFront Functions (Days of Frustration)
&lt;/h2&gt;

&lt;p&gt;My first thought: "CloudFront Functions are faster and cheaper than &lt;a href="mailto:Lambda@Edge"&gt;Lambda@Edge&lt;/a&gt;. Let's use those!"&lt;/p&gt;

&lt;p&gt;CloudFront Functions seemed perfect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Execute in microseconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cost a fraction of Lambda@Edge&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Native CloudFront integration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Perfect for request/response manipulation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spent days trying to make them work. Here's what I built:&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;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Bot detection works fine&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/facebookexternalhit|linkedinbot|twitterbot/&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="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// But now what? How do I redirect to prerender.io?&lt;/span&gt;
        &lt;span class="c1"&gt;// Can I change the origin? No.&lt;/span&gt;
        &lt;span class="c1"&gt;// Can I make an external call? No.&lt;/span&gt;
        &lt;span class="c1"&gt;// Can I modify the request to go elsewhere? No.&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;request&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;I tried multiple approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modifying the request URI&lt;/strong&gt; - CloudFront Functions can change URIs, but not the actual origin server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adding custom headers&lt;/strong&gt; - Headers were added successfully, but no way to act on them at the origin level&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Request transformation tricks&lt;/strong&gt; - Every creative workaround hit the same wall&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Hard Truth:&lt;/strong&gt; CloudFront Functions are incredibly limited by design. They can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ Modify headers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Change URIs and query strings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Validate and sanitize requests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Cannot change origins&lt;/strong&gt; (the actual server handling the request)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Cannot make external API calls&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Cannot perform complex routing logic&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're designed for lightweight tasks like adding security headers or URL rewrites, not for dynamically routing traffic to different services based on conditions.&lt;/p&gt;

&lt;p&gt;After days of testing, researching, and hitting dead ends, I realised: &lt;strong&gt;CloudFront Functions simply cannot solve this problem.&lt;/strong&gt; I could detect bots perfectly, but I had no way to route them to &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; CloudFront Functions are blazing fast and cheap, but their limitations are real. For origin switching based on conditions, Lambda@Edge is the only option.&lt;/p&gt;

&lt;p&gt;Time to learn Lambda@Edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: The 502 Bad Gateway Mystery
&lt;/h2&gt;

&lt;p&gt;This time, the function ran but returned &lt;strong&gt;502 errors&lt;/strong&gt;. CloudWatch logs showed the function was executing, but CloudFront rejected the response.&lt;/p&gt;

&lt;p&gt;The culprit? I was modifying the request structure incorrectly. Lambda@Edge has strict validation for the request/response objects you return. My custom origin configuration had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Missing required fields&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incorrect URL encoding&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong domain references (I was using the internal Amplify domain instead of the CloudFront domain)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each iteration meant another 15-minute deployment wait. Testing edge functions is &lt;em&gt;slow&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough: RTFM (Read The Fine Manual)
&lt;/h2&gt;

&lt;p&gt;Frustrated, I finally dove into &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;'s official documentation. They had a CloudFormation template specifically for CloudFront integration: &lt;code&gt;prerender-cloudfront.yaml&lt;/code&gt;. and thanks to Amazon Q developer CLI for debugging and fixing this issue.&lt;/p&gt;

&lt;p&gt;The key insight I'd been missing: &lt;strong&gt;Use the same Lambda function for TWO different CloudFront events:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;viewer-request&lt;/strong&gt;: Detect bots and add special headers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;origin-request&lt;/strong&gt;: Check for those headers and redirect to &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the beautiful simplicity of the final solution:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&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;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-host&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;// This is the origin-request function - redirect to prerender.io&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Redirecting to prerender.io&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-query-string&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-query-string&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service.prerender.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;keepaliveTimeout&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="na"&gt;customHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
                &lt;span class="na"&gt;sslProtocols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TLSv1&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;TLSv1.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/https%3A%2F%2F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-host&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;value&lt;/span&gt;
            &lt;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="c1"&gt;// This is the viewer-request function - detect bots and set headers&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;user_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&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;user_agent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/googlebot|adsbot&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;google|Feedfetcher&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;Google|bingbot|yandex|baiduspider|Facebot|facebookexternalhit|twitterbot|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|redditbot|applebot|whatsapp|flipboard|tumblr|bitlybot|skypeuripreview|nuzzel|discordbot|google page speed|qwantify|pinterestbot|bitrix link preview|xing&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;contenttabreceiver|chrome&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;lighthouse|telegrambot|Perplexity|OAI-SearchBot|ChatGPT|GPTBot|ClaudeBot|Amazonbot|integration-test/i&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="nx"&gt;user_agent&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/_escaped_fragment_/&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prerender&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/i&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&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;prerender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bot detected:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user_agent&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_PRERENDER_TOKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}];&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-host&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;host&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;value&lt;/span&gt;&lt;span class="p"&gt;}];&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-prerender-cachebuster&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Prerender-Cachebuster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()}];&lt;/span&gt;
                &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-query-string&lt;/span&gt;&lt;span class="dl"&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Query-String&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Regular user&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="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;callback&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="nx"&gt;request&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;
  
  
  Why This Works (The Two-Stage Magic)
&lt;/h2&gt;

&lt;p&gt;The genius of this approach is the two-stage processing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 - Viewer Request (Edge → Client):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Lambda checks the user agent&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If it's a bot, adds special headers (&lt;code&gt;x-prerender-token&lt;/code&gt;, &lt;code&gt;x-prerender-host&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Passes request along&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 - Origin Request (Edge → Origin):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Same Lambda function checks for those special headers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If present, redirects the request to &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; instead of Amplify&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; renders the Angular app and returns HTML&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If not present, request goes directly to Amplify (regular users)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ Regular users never touch the prerender service (fast!)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Bots get fully rendered HTML with proper meta tags&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Zero changes to our Amplify hosting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Cost-efficient—only pay for actual bot traffic (~1%)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring CloudFront
&lt;/h2&gt;

&lt;p&gt;In the CloudFront distribution settings, I associated the Lambda function with &lt;strong&gt;both&lt;/strong&gt; events:&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;Associations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;EventType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;viewer-request&lt;/span&gt;
    &lt;span class="na"&gt;LambdaFunctionARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:lambda:us-east-1:xxx:function:socialbots:1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;EventType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;origin-request&lt;/span&gt;
    &lt;span class="na"&gt;LambdaFunctionARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:lambda:us-east-1:xxx:function:socialbots:1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same function, two different trigger points.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Angular Side: Signaling Readiness
&lt;/h2&gt;

&lt;p&gt;One more piece: Angular needed to tell &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; when the page was fully rendered with all meta tags set.&lt;/p&gt;

&lt;p&gt;In our article component:&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="nf"&gt;ngOnInit&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;articleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paramMap&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&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;// Update meta tags&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og:title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og:description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og:image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateTag&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twitter:card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary_large_image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Signal to Prerender.io that the page is ready&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;prerenderReady&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;prerenderReady = true&lt;/code&gt;, &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; might snapshot the page before our API call completes and meta tags are set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and Debugging
&lt;/h2&gt;

&lt;p&gt;Testing edge functions is painful because of deployment times. Here's what helped:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CloudWatch Logs&lt;/strong&gt; Lambda@Edge logs go to CloudWatch in the region where the function executes (us-east-1 for me):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/aws/lambda/us-east-1.socialbots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Direct curl Testing&lt;/strong&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;# Test bot detection&lt;/span&gt;
curl &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"facebookexternalhit/1.1"&lt;/span&gt; https://your-domain.com/article/123

&lt;span class="c"&gt;# Test regular user&lt;/span&gt;
curl &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0"&lt;/span&gt; https://your-domain.com/article/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Cache Invalidation&lt;/strong&gt; CloudFront caches everything. After changes, invalidate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudfront create-invalidation &lt;span class="nt"&gt;--distribution-id&lt;/span&gt; YOUR_DIST_ID &lt;span class="nt"&gt;--paths&lt;/span&gt; &lt;span class="s2"&gt;"/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;&lt;strong&gt;Prerender.io&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;Direct Testing&lt;/strong&gt; Check what &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://service.prerender.io/https://your-domain.com/article/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Waiting Game
&lt;/h2&gt;

&lt;p&gt;The hardest part? &lt;strong&gt;Patience.&lt;/strong&gt; Every CloudFront distribution update takes 10-15 minutes to propagate. Every Lambda@Edge deployment requires replicating to all edge locations.&lt;/p&gt;

&lt;p&gt;I learned to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Make changes in small batches&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test thoroughly in CloudWatch before deploying&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;'s direct API for quick validation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep a testing checklist to avoid forgetting edge cases&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't abandon a great platform for one missing feature.&lt;/strong&gt; Amplify is excellent—we just needed to extend it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda@Edge is powerful but picky.&lt;/strong&gt; CommonJS only, strict validation, slow deployments. Plan accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Two-stage processing is elegant.&lt;/strong&gt; Using the same function for both viewer-request and origin-request is cleaner than complex routing logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Follow official patterns.&lt;/strong&gt; &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt;'s CloudFormation template saved me hours of trial and error. When stuck, check the docs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache management is critical.&lt;/strong&gt; Both CloudFront and &lt;a href="http://Prerender.io" rel="noopener noreferrer"&gt;Prerender.io&lt;/a&gt; cache aggressively. Know how to clear both.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing takes time.&lt;/strong&gt; Budget for 15-minute deployment cycles when working with edge functions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;What started as "our social links don't work" turned into a deep dive into CloudFront, Lambda@Edge, and edge computing. The journey had plenty of 502 errors, syntax mistakes, and waiting for deployments.&lt;/p&gt;

&lt;p&gt;But the end result? Our Angular SPA on Amplify now provides beautiful social media previews while maintaining the performance and deployment simplicity we loved in the first place.&lt;/p&gt;

&lt;p&gt;Sometimes the right solution isn't changing your infrastructure—it's extending what you already have.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you dealt with similar challenges in your SPA deployments? What solutions worked for you? Let me know in the comments!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References :
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/AvinashDalvi89/cloudfront-lambda-edge-prerender-io-routing" rel="noopener noreferrer"&gt;https://github.com/AvinashDalvi89/cloudfront-lambda-edge-prerender-io-routing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.prerender.io/docs/cloudfront" rel="noopener noreferrer"&gt;https://docs.prerender.io/docs/cloudfront&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>amplify</category>
      <category>aws</category>
      <category>prerender</category>
    </item>
    <item>
      <title>The 9 AM Discovery That Saved Our Production: An ECS Fargate Circuit Breaker Story</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Thu, 18 Sep 2025 05:35:43 +0000</pubDate>
      <link>https://forem.com/aws-builders/the-9-am-discovery-that-saved-our-production-an-ecs-fargate-circuit-breaker-story-ekb</link>
      <guid>https://forem.com/aws-builders/the-9-am-discovery-that-saved-our-production-an-ecs-fargate-circuit-breaker-story-ekb</guid>
      <description>&lt;p&gt;Hello Devs,&lt;/p&gt;

&lt;p&gt;In the world of containerised deployments, small mistakes can have catastrophic consequences. What started as a routine morning API test in our development environment turned into a revelation about production resilience that fundamentally changed how we approach &lt;strong&gt;ECS Fargate&lt;/strong&gt; deployments.&lt;/p&gt;

&lt;p&gt;This is the story of how a simple port configuration error taught us the critical importance of &lt;strong&gt;ECS Deployment Circuit Breakers&lt;/strong&gt; – and why every team running workloads on &lt;strong&gt;AWS Fargate&lt;/strong&gt; should consider them essential infrastructure, not optional extras.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The best production incidents, as it turns out, are the ones that never happen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Our Flask API ran smoothly on ECS Fargate with a cost-optimized dev setup — tasks auto-started at 8 AM and stopped after hours using CloudWatch alarms.&lt;/p&gt;

&lt;p&gt;We used an Application Load Balancer (ALB) targeting port &lt;code&gt;5001&lt;/code&gt;, with health checks and task definitions perfectly aligned:&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;# app.py - The way it had always been
&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&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="nf"&gt;run&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&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;5001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ECS config:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Container port:&lt;/strong&gt; 5001&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Target group:&lt;/strong&gt; 5001&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ALB health checks:&lt;/strong&gt; 5001&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything in harmony.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Innocent Change
&lt;/h2&gt;

&lt;p&gt;One of our backend developers, was working late on a new feature. They were running multiple services locally and kept hitting port conflicts. Port 5001 was already occupied by another service.&lt;/p&gt;

&lt;p&gt;"Quick fix," developer thought, and made what seemed like the most logical change:&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;# app.py - The "harmless" local change
&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&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="nf"&gt;run&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&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;5201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Changed to avoid local conflict
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature worked perfectly in her local environment. Tests passed. Code review looked good. The Docker build succeeded. Everything seemed normal.&lt;/p&gt;

&lt;p&gt;But here's where the story takes a turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 9 AM Discovery
&lt;/h2&gt;

&lt;p&gt;The next morning, I arrived open my machine around 9 AM and decided to run some API tests before diving into feature work. Our automated CloudWatch alarm had dutifully started the ECS Fargate tasks at 8 AM, just as configured. But something was wrong.&lt;/p&gt;

&lt;p&gt;Every API call returned the dreaded &lt;strong&gt;502 Bad Gateway&lt;/strong&gt; error.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757422966125%2F84d830de-418d-4257-b4e3-bc2e73b3fe84.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757422966125%2F84d830de-418d-4257-b4e3-bc2e73b3fe84.png" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I immediately checked the &lt;strong&gt;ECS console&lt;/strong&gt;, and what I saw made me to think : &lt;strong&gt;Fargate tasks&lt;/strong&gt; were in a continuous cycle of PENDING → RUNNING → STOPPED. They would start up, run for a few minutes, then get drained and terminated, only for &lt;strong&gt;ECS&lt;/strong&gt; to immediately spin up new ones.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757420248853%2F80d05d3f-d8af-482c-b5d3-9182dd9f5b8c.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757420248853%2F80d05d3f-d8af-482c-b5d3-9182dd9f5b8c.png" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The root cause hit me like a lightning bolt: Our Flask application was now listening on port 5201, but everything else in our infrastructure was still configured for port 5001.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Downward Spiral
&lt;/h2&gt;

&lt;p&gt;What followed was a textbook example of how a small misconfiguration can cascade into a major incident:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Task Launch&lt;/strong&gt;: &lt;strong&gt;ECS Fargate&lt;/strong&gt; would start a new task&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Health Check Failure&lt;/strong&gt;: ALB couldn't reach the app on port 5001&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Task Termination&lt;/strong&gt;: &lt;strong&gt;ECS&lt;/strong&gt; marked the task as unhealthy and terminated it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replacement Attempt&lt;/strong&gt;: &lt;strong&gt;ECS&lt;/strong&gt; immediately launched a new &lt;strong&gt;Fargate task&lt;/strong&gt; to maintain desired count&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infinite Loop&lt;/strong&gt;: Steps 1-4 repeated endlessly&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our &lt;strong&gt;ECS Fargate cluster&lt;/strong&gt; was stuck in what we later dubbed "the task death spiral." New &lt;strong&gt;Fargate tasks&lt;/strong&gt; were being created and destroyed every few minutes, consuming compute resources while serving zero traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Circuit Breaker Revelation
&lt;/h2&gt;

&lt;p&gt;During our post-incident analysis, then I realised something that would change our deployment strategy forever: "What if I told you this entire incident could have been prevented automatically?"&lt;/p&gt;

&lt;p&gt;Enter the &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html" rel="noopener noreferrer"&gt;&lt;strong&gt;ECS Fargate Deployment Circuit Breaker&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This AWS feature acts like an intelligent safety net for &lt;strong&gt;ECS Fargate&lt;/strong&gt; deployments. When enabled, it monitors your &lt;strong&gt;Fargate&lt;/strong&gt; deployment and can automatically detect when something is going wrong, stopping the deployment and rolling back to the previous stable version.&lt;/p&gt;

&lt;h2&gt;
  
  
  How ECS Behaves in Different Rollback Scenarios
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Desired Count&lt;/th&gt;
&lt;th&gt;Task Def Changed&lt;/th&gt;
&lt;th&gt;Rollback Triggered&lt;/th&gt;
&lt;th&gt;Rollback Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Broken image pushed with &lt;code&gt;latest&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken task def v3 (flask-app:v2)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;⏱ ~10–20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same failure with &lt;code&gt;desiredCount=5&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;⏱ ~3–5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Circuit breaker &lt;strong&gt;only works&lt;/strong&gt; if a new &lt;strong&gt;task definition is registered&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Desired count = 1&lt;/strong&gt; leads to &lt;strong&gt;slow failure detection&lt;/strong&gt;, delaying rollback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ECS uses an internal failure threshold (usually 3 failed tasks).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Circuit Breaker Would Have Saved Us
&lt;/h2&gt;

&lt;p&gt;Let's replay our incident with &lt;strong&gt;ECS Fargate deployment circuit breaker&lt;/strong&gt; enabled:&lt;/p&gt;

&lt;p&gt;To better understand how ECS identifies and reacts to a bad deployment, here’s a simplified flow diagram based on our real incident:&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757425452103%2Ff2adce56-6da8-4d09-bff8-36a5d0a4fa6c.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757425452103%2Ff2adce56-6da8-4d09-bff8-36a5d0a4fa6c.png" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deployment Start:&lt;/strong&gt; Mismatched port in new task definition&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitoring Begins:&lt;/strong&gt; ECS tracks task health and startup patterns&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failure Detected:&lt;/strong&gt; Multiple ECS task failures trigger threshold&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic Rollback:&lt;/strong&gt; ECS reverts to previous task definition&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Service Restored:&lt;/strong&gt; Traffic resumes via healthy version&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of 75 minutes of downtime, we would have had perhaps 5-10 minutes of degraded performance while the circuit breaker detected and resolved the issue.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757423154034%2F03f2b8c7-0997-4ddf-bde9-b1484ed07603.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757423154034%2F03f2b8c7-0997-4ddf-bde9-b1484ed07603.png" width="800" height="222"&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757423625401%2F2ab3d6ac-17a2-4456-ad57-54715a949a74.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757423625401%2F2ab3d6ac-17a2-4456-ad57-54715a949a74.png" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How ECS Actually Triggers Rollbacks: Behind the Scenes
&lt;/h2&gt;

&lt;p&gt;During our experiments, we noticed some undocumented behaviours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ECS doesn't rollback unless there's a new &lt;strong&gt;task definition&lt;/strong&gt; — pushing a new image to &lt;code&gt;latest&lt;/code&gt; doesn’t count.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Desired count = 1&lt;/strong&gt; (common in off-hours cost optimisation) leads to much slower rollbacks due to staggered failures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ECS seems to use a dynamic &lt;strong&gt;failure threshold of 3&lt;/strong&gt; (confirmed visually in the console), meaning it waits for 3 failed task launches before triggering rollback. You cannot change either of the threshold values. It is mentioned in the &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html#failure-threshold" rel="noopener noreferrer"&gt;ECS deployment circuit breaker&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ECS uses the fol&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html" rel="noopener noreferrer"&gt;l&lt;/a&gt;owing logic to determine rollback&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html" rel="noopener noreferrer"&gt;:&lt;br&gt;&lt;br&gt;
&lt;/a&gt;Minimum threshold &amp;lt;= 0.5 * &lt;code&gt;desired task count&lt;/code&gt; =&amp;gt; maximum threshold&lt;/p&gt;
&lt;/blockquote&gt;
&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757422632526%2F3736e4ef-0e1f-4f16-8a60-06dc99bb5a1a.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1757422632526%2F3736e4ef-0e1f-4f16-8a60-06dc99bb5a1a.png" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What this means in practice:&lt;br&gt;&lt;br&gt;
Even if circuit breaker is “enabled,” rollback &lt;strong&gt;won’t happen&lt;/strong&gt; unless you structure your deployments correctly.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Circuit Breaker Implementation
&lt;/h2&gt;

&lt;p&gt;That afternoon, we made a decision that would prove to be one of our best infrastructure investments: enabling ECS Deployment Circuit Breaker across all our services, starting with our most critical production workloads.&lt;/p&gt;

&lt;p&gt;The configuration was surprisingly straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deploymentCircuitBreaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rollback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;What Happens When DesiredCount = 0? A Real Risk Pattern&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our incident happened in a development environment using off-hours scaling — every night, &lt;code&gt;desiredCount = 0&lt;/code&gt;, and each morning ECS spins the tasks back up at 8 AM. This helps save cost during non-business hours.&lt;/p&gt;

&lt;p&gt;But here’s the hidden danger we uncovered through real experiments:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our real case, we pushed a new (and broken) image to &lt;code&gt;flask-app:latest&lt;/code&gt; overnight.&lt;br&gt;&lt;br&gt;
However, we didn’t register a new task definition — the task definition was unchanged.&lt;br&gt;&lt;br&gt;
So when ECS scaled up in the morning, it pulled the broken image and launched new tasks.&lt;br&gt;&lt;br&gt;
Because ECS had no healthy task running and no “new deployment” to monitor, &lt;strong&gt;no rollback happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This subtle but critical issue means that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ECS had &lt;strong&gt;no baseline healthy task&lt;/strong&gt; to compare against&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There was &lt;strong&gt;no new task definition&lt;/strong&gt;, so ECS didn’t consider this a deployment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Circuit breaker logic was &lt;strong&gt;never triggered&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ECS just kept retrying the same broken image silently&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even with circuit breaker enabled, &lt;strong&gt;rollback only works if ECS sees a new deployment&lt;/strong&gt; (i.e., new task definition revision). In our case, since we reused &lt;code&gt;flask-app:latest&lt;/code&gt; with the same task definition, ECS had nothing to roll back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations (Based on Real-World Failures)
&lt;/h2&gt;

&lt;p&gt;These are not just best practices from the AWS documentation. These are hard-earned lessons from our own experiments and real incident recoveries.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Avoid using &lt;code&gt;latest&lt;/code&gt; tag in ECS task definitions
&lt;/h3&gt;

&lt;p&gt;ECS won't detect image changes if you're using &lt;code&gt;flask-app:latest&lt;/code&gt; and don’t update the task definition. This can silently deploy broken images &lt;strong&gt;without triggering a rollback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do this instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use &lt;strong&gt;immutable image tags&lt;/strong&gt; like &lt;code&gt;v1.2.3&lt;/code&gt;, &lt;code&gt;build-20250909&lt;/code&gt;, or a full &lt;strong&gt;SHA digest&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Always reference a new task definition revision tied to each deployment&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Register a new task definition with every deployment
&lt;/h3&gt;

&lt;p&gt;The deployment circuit breaker &lt;strong&gt;only activates when ECS detects a new deployment&lt;/strong&gt;. If the task definition remains unchanged (even with a new image), ECS won’t treat it as a deployment, and rollback won’t occur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do this instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automate task definition registration in your CI/CD pipeline&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Even if using the same image tag, register a revision to trigger deployment detection&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Use CloudWatch alarms to detect deployment failures early
&lt;/h3&gt;

&lt;p&gt;ECS retries silently when tasks fail during deployment. In non-prod or low-desiredCount environments, this can go unnoticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do this instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Monitor &lt;code&gt;UnhealthyHostCount&lt;/code&gt; (ALB) and &lt;strong&gt;ECS service deployment events&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alert on unusual &lt;strong&gt;task exit reasons&lt;/strong&gt;, &lt;strong&gt;STOPPED&lt;/strong&gt; states, or drops in &lt;code&gt;RunningTaskCount&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Enforce Task Definition Updates in CI/CD
&lt;/h3&gt;

&lt;p&gt;One common issue we saw: devs pushed new images to &lt;code&gt;latest&lt;/code&gt;, but forgot to update task definitions. Result? No rollback, no detection, broken app silently running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do this instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a CI/CD check: &lt;strong&gt;fail the pipeline&lt;/strong&gt; if task definition revision isn't updated&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintain an audit log: map every deployment to a task definition revision&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Use higher &lt;code&gt;desiredCount&lt;/code&gt; during deployments for faster rollback
&lt;/h3&gt;

&lt;p&gt;In our tests, when &lt;code&gt;desiredCount&lt;/code&gt; was set to 1, rollback took over 20 minutes to trigger. With &lt;code&gt;desiredCount&lt;/code&gt; set to 5, the circuit breaker detected the failure pattern faster and triggered rollback within 3–5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Temporarily increase &lt;code&gt;desiredCount&lt;/code&gt; during deployments (e.g., from 1 to 5).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alternatively, tune &lt;code&gt;deploymentConfiguration&lt;/code&gt; to use &lt;code&gt;maxPercent = 200&lt;/code&gt; and &lt;code&gt;minHealthyPercent = 50&lt;/code&gt; to allow parallel task launches during updates.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Summary Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ECS didn’t rollback on broken image&lt;/td&gt;
&lt;td&gt;Always register a new task definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS used broken &lt;code&gt;latest&lt;/code&gt; tag silently&lt;/td&gt;
&lt;td&gt;Avoid &lt;code&gt;latest&lt;/code&gt;, use immutable image tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow rollback when desired count = 1&lt;/td&gt;
&lt;td&gt;Use higher &lt;code&gt;desiredCount&lt;/code&gt; during deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No alert when tasks failed&lt;/td&gt;
&lt;td&gt;Add CloudWatch alarms for task health and service events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment skipped task def update&lt;/td&gt;
&lt;td&gt;Enforce task def registration in pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion: The Safety Net We Proactively Built
&lt;/h2&gt;

&lt;p&gt;Even small mistakes — like a port mismatch — can bring down containerized systems. That’s why we treat circuit breakers not as optional features, but as must-have infrastructure. They're not just for rollback — they build resilience into your deployment lifecycle.&lt;/p&gt;

&lt;p&gt;That seemingly minor port change could’ve caused hours of downtime — but it didn’t. Because we caught it early, we had the chance to rethink our deployment safety.&lt;/p&gt;

&lt;p&gt;We turned that morning’s incident into a proactive defense strategy by enabling ECS Deployment Circuit Breaker across all services. It now gives us confidence that even if a broken deployment slips through, ECS will detect the issue and roll back automatically — without us scrambling at 9 AM.&lt;/p&gt;

&lt;p&gt;Our team now deploys with confidence, not caution. And the best part? The incident never reached users.&lt;/p&gt;

&lt;p&gt;Sometimes the best production incidents are the ones that never happen.&lt;/p&gt;

&lt;p&gt;👉 Have you faced something similar? Let’s talk in the comments.&lt;/p&gt;

&lt;h3&gt;
  
  
  References:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/containers/announcing-amazon-ecs-deployment-circuit-breaker/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/containers/announcing-amazon-ecs-deployment-circuit-breaker/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ecs</category>
      <category>fargate</category>
      <category>docker</category>
    </item>
    <item>
      <title>AWS Kiro IDE: Not Just Another AI Toy for Developers</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Tue, 15 Jul 2025 04:40:56 +0000</pubDate>
      <link>https://forem.com/aws-builders/aws-kiro-ide-not-just-another-ai-toy-for-developers-595h</link>
      <guid>https://forem.com/aws-builders/aws-kiro-ide-not-just-another-ai-toy-for-developers-595h</guid>
      <description>&lt;p&gt;Hello Devs,&lt;/p&gt;

&lt;p&gt;Every morning, after my usual routine, I scroll through Reddit. It’s part habit, part curiosity—a way to catch up on what’s happening in the world of AWS and product building.&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%2Fm7nshc1gkgaqy1jkdyyl.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%2Fm7nshc1gkgaqy1jkdyyl.png" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But let me be clear: I’m not the kind of developer who jumps on every trendy new tool the moment it launches. Unless I see a clear reason it could help me in my real work, I usually wait and watch.&lt;/p&gt;

&lt;p&gt;That’s because right now, I’m working under a strict timeline for GTM (Go-To-Market). I’m constantly evaluating anything that might help me ship faster without sacrificing quality.&lt;/p&gt;

&lt;p&gt;I have Figma designs ready for most of our screens. Just last week, I built out an entire chat module—including both the UI and backend APIs—using ChatGPT to help speed up the process. Tools like that are no longer just experiments for me; they’re part of how I stay on track to deliver.&lt;/p&gt;

&lt;p&gt;So when I saw AWS’s Reddit post about Kiro IDE, I wasn’t sure I’d even try it. Another AI tool? There’s been a flood of those lately.&lt;/p&gt;

&lt;p&gt;But there was something different about how AWS positioned Kiro. They weren’t just talking about generating code. They were talking about helping developers go all the way from ideas and specs to production systems. That’s exactly where most AI tools fall short.&lt;/p&gt;

&lt;p&gt;Given the tight timelines I’m working under, I figured: If this can help me move faster for real product work, it’s worth testing. So I decided to give it a shot—using a real feature I’m building for NuShift Connect.&lt;/p&gt;

&lt;p&gt;At NuShift Connect, we’re building a health-focused social platform. Recently, I’ve been working on an additional feature: Groups. It’s a way for users to create dedicated communities around health topics—like cancer awareness, fitness journeys, or wellness discussions.&lt;/p&gt;

&lt;p&gt;This Groups feature isn’t my entire product. It’s one of many pieces I’m layering onto an existing, fairly complex codebase. And that’s why I usually don’t rush into brand-new tools. I can’t afford to break my momentum or disrupt working systems.&lt;/p&gt;

&lt;p&gt;But Kiro IDE made me curious because it seemed like it might help me structure my feature properly from the start, keep my thinking organised as I integrate with existing code, and speed up writing UI components in Angular.&lt;/p&gt;

&lt;p&gt;I was at the GenAI Loft last week, where I attended a session on the AI Development Lifecycle (AIDLC) by &lt;a href="https://www.linkedin.com/in/siddhesh-jog/" rel="noopener noreferrer"&gt;Siddhesh Jog&lt;/a&gt;. He talked about how successful product development often requires moving through clear phases—from ideation and requirement gathering to building, testing, and deploying. As I explored Kiro, I could see that it’s built with that same &lt;a href="https://www.linkedin.com/in/siddhesh-jog/" rel="noopener noreferrer"&gt;&lt;/a&gt;philosophy in mind. It feels like a tool designed to guide product builders step by step through that lifecycle, rather than just spitting out random code&lt;/p&gt;

&lt;p&gt;One thing that stood out right away was Kiro’s idea of Specs. Instead of just throwing random prompts at an AI, Kiro pushes you to define what you’re building—almost like writing requirements documentation but right inside your IDE.&lt;/p&gt;

&lt;p&gt;For the Groups feature, I wrote out a spec something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Users can create new groups with a name, description, and cover image&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Groups can be public or private&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each group has posts, comments, and member lists&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We already have user profiles and authentication in place&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;My initial focus is designing the UI and Angular components—not backend APIs yet&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment I saved this spec, Kiro started suggesting Angular component structures (Group List, Group Detail, Create Group form), folder organization ideas for modularity, and approaches for handling reactive forms and UI state.&lt;/p&gt;

&lt;p&gt;Instead of dumping generic code snippets, Kiro was shaping its suggestions around my actual app structure and requirements.&lt;/p&gt;

&lt;p&gt;This was the part that felt genuinely useful. I’ve used plenty of AI tools before—like when I built our chat module last week using ChatGPT to generate both frontend components and backend API scaffolding. That experience showed me how much time AI can save if used well.&lt;/p&gt;

&lt;p&gt;Kiro felt similar, but with even more context awareness for Angular. It suggested splitting my UI into smart, modular components. It helped me outline reactive forms for group creation, including validation logic. It offered ways to connect state between components. It even reminded me to think about loading states and empty UI screens.&lt;/p&gt;

&lt;p&gt;I haven’t wired up the backend yet—that’s the next step. But just for the Angular side, Kiro saved me hours of manual scaffolding and planning.&lt;/p&gt;

&lt;p&gt;Another thing that impressed me was how Kiro handles Hooks and Steering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hooks&lt;/strong&gt; are like background automations. For example, when I updated my spec, Kiro prompted me to adjust my component interfaces and possibly update tests. Small nudges, but useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steering&lt;/strong&gt; lets you guide how the AI writes code. I could tell Kiro to stick to Angular best practices, use TypeScript consistently, and avoid certain libraries we don’t use at NuShift. Instead of me manually rewriting AI code every time, Kiro started adapting to my way of working.&lt;/p&gt;

&lt;p&gt;While exploring Kiro’s UI, I saw something called &lt;strong&gt;MCP Servers&lt;/strong&gt;. From what I gather, this lets Kiro connect to real tools and data sources, like fetching live DynamoDB schemas, reading API specs, or integrating with CI/CD systems. I haven’t tested this yet, but the idea that Kiro could be aware of my real environment—not just write generic code—is intriguing for the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s Still Early, But It Feels Different
&lt;/h2&gt;

&lt;p&gt;Like any new tool, Kiro isn’t perfect yet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Sometimes its suggestions were too generic, especially for UI styling details. Even when I gave it Figma screenshots, it didn’t always pick up the exact styles or color codes. I found myself needing to do more prompting to match our design system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To be fair, this is something I could solve better by using Kiro’s Specs feature upfront. You can define your product’s brand guidelines, color palettes, typography, and other design rules directly in specs. I simply hadn’t included those details in my initial prompts—but next time, I plan to try it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It occasionally misunderstood the relationships between my existing components. For example, our codebase has some duplicate components and modules left over from older versions, with similar names scattered in different folders. Instead of deleting unused modules, previous developers just added new ones alongside them. That created confusion for Kiro, which sometimes pulled from the wrong places.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It’s a reminder that tools like Kiro work best if your codebase is clean—or at least if you set ground rules or do some upfront cleanup so the AI knows what’s current and what’s legacy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Even when there was an error running &lt;code&gt;ng serve&lt;/code&gt;, Kiro didn’t automatically detect it or suggest fixes. I had to explicitly prompt it to run the command, read the error output, and help debug the issue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I was thinking it would be amazing if Kiro could automatically stop &lt;code&gt;ng serve&lt;/code&gt; after making changes, rerun it, and check for errors. But I also realize that might get tricky. If the error doesn’t get fixed properly, it could send Kiro into an infinite loop of stopping, starting, and trying again—something I’ve definitely seen happen with ChatGPT during debugging.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven’t tested how well it handles full-stack API integrations yet.&lt;/p&gt;

&lt;p&gt;That said, it’s worth remembering Kiro is brand new, and everyone—from developers to tech media—is still exploring what it can really do.&lt;/p&gt;

&lt;p&gt;But even with those limitations, I came away feeling like this is not just another AI toy.&lt;/p&gt;

&lt;p&gt;For the first time, I felt like an AI tool was working with me to build a real feature—not just generating isolated snippets of code.&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%2F6axrv8hs3bn03xzasrc0.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%2F6axrv8hs3bn03xzasrc0.png" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Product Builders Should Pay Attention
&lt;/h2&gt;

&lt;p&gt;I’m usually cautious about new tools, especially trendy ones. But Kiro felt different because it fits how product builders actually work.&lt;/p&gt;

&lt;p&gt;We don’t just write code—we design systems and think about integration. We have existing codebases, not blank slates. We need speed—but also clarity and maintainability.&lt;/p&gt;

&lt;p&gt;Kiro helped me think through my Groups feature in a structured way. It saved time on my Angular scaffolding. And it kept me focused on building something that fits into NuShift Connect—not just playing with isolated code.&lt;/p&gt;

&lt;p&gt;I have a strict GTM timeline and a long list of features to deliver. Recently, AI tools like ChatGPT have helped me build faster—like with our new chat module. But Kiro feels like a step forward because it’s built for product builders, not just for writing isolated pieces of code.&lt;/p&gt;

&lt;p&gt;If AWS keeps evolving it, Kiro could be one of the most useful developer tools they’ve launched since Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Takeaway
&lt;/h2&gt;

&lt;p&gt;My advice? If you’re curious, don’t just read blog posts (even this one). Pick a real feature you’re working on and try building it in Kiro IDE. That’s when you’ll see whether it’s just another AI experiment—or a glimpse of how we’ll build products in the future.&lt;/p&gt;

&lt;p&gt;Next, I’m planning to see how Kiro handles backend integrations for Groups—especially how it manages API design and DynamoDB schemas. I’ll share those results once I’ve tested it further. Right now, Kiro IDE is free during its preview period. I’m not sure what pricing AWS will introduce later—but for now, it’s worth trying if you’re curious.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: Kiro IDE isn’t just an AI code generator. It feels like a real partner helping me think through product features and ship faster under tight deadlines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References Worth Exploring
&lt;/h2&gt;

&lt;p&gt;If you’re curious to dig deeper into Kiro IDE, here are some good places to start:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/aws/introducing-kiro-the-agentic-ai-ide/" rel="noopener noreferrer"&gt;Introducing Kiro: The Agentic AI IDE (AWS Blog)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro IDE Official Site &amp;amp; Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/kiro-the-new-agentic-ai-ide-from-aws-5311"&gt;Kiro: The new Agentic AI IDE from AWS (DEV.to)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/kiro-agentic-ai-ide-beyond-a-coding-assistant-full-stack-software-development-with-spec-driven-220l"&gt;Kiro Agentic AI IDE — Beyond a coding assistant (DEV.to)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>aws</category>
      <category>ai</category>
      <category>aitools</category>
    </item>
    <item>
      <title>Migrating from EC2 to Containers: What Teams Miss</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Thu, 10 Jul 2025 16:28:44 +0000</pubDate>
      <link>https://forem.com/aws-builders/migrating-from-ec2-to-containers-what-teams-miss-4di5</link>
      <guid>https://forem.com/aws-builders/migrating-from-ec2-to-containers-what-teams-miss-4di5</guid>
      <description>&lt;p&gt;Hello Devs,&lt;/p&gt;

&lt;p&gt;In this blog, we are going to learn about the real challenges, insights and mistakes behind migrating from EC2 to containers, based on my experience. So let’s start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality Check That Started It All
&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%2Flldrkgly7b78e1oobrsu.gif" 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%2Flldrkgly7b78e1oobrsu.gif" width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the honest truth: Before NuShift, I had never migrated workloads from EC2 to containers. At previous jobs, we were either stuck in the EC2 era, had no real container strategy, or everything was already set up for containers, and my role was to focus on scaling and improving as a developer since the DevOps team handled it. I have been using containers for a long time and understood their benefits, but I never had the chance to lead that transformation..&lt;/p&gt;

&lt;p&gt;When I joined NuShift, my first few months were spent doing the unglamorous work—cleaning up unused resources, right-sizing EC2 instances, and optimising our AWS bill. We managed to improve utilisation, but I knew this was just putting a band-aid on a deeper problem.&lt;/p&gt;

&lt;p&gt;That's when I set a personal and team goal: move us toward containers. This wasn't about cost savings—it was about solving real operational headaches. We needed better resource utilisation, more structured deployments, and most importantly, the promise that "build once, run anywhere" that containers offered.&lt;/p&gt;

&lt;p&gt;Like most developers, our instinct was simple: write code, host it quickly, pick the easiest option. That usually meant EC2. Even at NuShift, we initially launched everything on EC2 instances. We even started considering Graviton-based instances for better performance, but realised that would mean dealing with architecture changes and potential compatibility issues.&lt;/p&gt;

&lt;p&gt;The real pain point? Environment consistency. What worked in development didn't always work in staging. Missing Python packages, different system libraries, manual patching cycles—these weren't just inconveniences, they were blocking us from moving fast as we prepared for our public launch.&lt;/p&gt;

&lt;p&gt;That's when containers became part of my plan from day one. Not because of some dramatic failure, but because I could see the operational complexity we were heading toward if we didn't change course.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Operational Pain of "Simple" EC2 Deployments
&lt;/h2&gt;

&lt;p&gt;Launching a product is messy. You want the simplest path to get things running. That’s why EC2 becomes the go-to for most startup teams. At NuShift, our early backend stack—Flask APIs, WebSocket server, and MySQL—was each running on separate EC2 instances. No orchestration. Minimal automation. It worked... until it didn’t. Here's what nobody tells you about the EC2-first approach: it feels simple until you need consistency across environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Development vs QA Nightmare
&lt;/h3&gt;

&lt;p&gt;Our development setup worked perfectly. Local Flask app, local database, everything smooth. But when we moved to QA ( we are not on production yet)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Missing Python packages that weren't in our requirements.txt&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Different system library versions causing unexpected behaviors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manual patching cycles that meant potential downtime&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"It works on my machine" became our team's unofficial motto&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst part? Setting up a new environment meant hours of manual configuration, hoping we didn't miss any dependencies that existed on our other servers.&lt;/p&gt;

&lt;p&gt;These challenges highlighted the need for a more consistent and reliable deployment process.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Deployment gamble game (a.k.a. Jugaad)
&lt;/h3&gt;

&lt;p&gt;Our deployment process was essentially gambling:&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;# Our "sophisticated" deployment process&lt;/span&gt;
ssh into qa-server
git pull
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt  &lt;span class="c"&gt;# hope nothing breaks or &lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;library-xss &lt;span class="c"&gt;#manually installed os level package&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart app
&lt;span class="c"&gt;# Check logs and pray&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every deployment felt like rolling the dice. Not because our code was bad—but because we could never guarantee the environment matched what we’d tested locally. A missing Python package here, a different system library version there—it was chaos waiting to happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters Beyond Just Code
&lt;/h3&gt;

&lt;p&gt;At first, we thought the problem was purely technical: missing packages, inconsistent environments, surprise crashes in staging. But underneath all that was a bigger issue:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;We were trying to build modern applications on infrastructure we were too small to manage properly.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deploying on EC2 meant we were responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;OS patching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Library dependencies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security hardening&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deployment orchestration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling and failover&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For large teams with dedicated DevOps or platform engineers, this might be manageable. For us—a small team of developers—it wasn’t sustainable.&lt;/p&gt;

&lt;p&gt;That’s when we realized:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure decisions aren’t just about technology—they’re about your team’s strengths and capacity.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Team Reality Check: Know Your Strengths
&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%2Fb8qewzzvumvg38d2miu5.gif" 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%2Fb8qewzzvumvg38d2miu5.gif" width="600" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the brutal truth most migration guides skip: your team composition matters more than technical specs.&lt;/p&gt;

&lt;p&gt;Before choosing any path, ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Are you a developer who also handles infrastructure?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do you have dedicated DevOps/Platform engineers?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How much time can you realistically spend on server maintenance?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At NuShift, we had 3 full-stack developers. Zero dedicated infrastructure people. This meant every hour spent patching EC2 instances, managing security updates, and troubleshooting server issues was an hour stolen from building features our users needed.&lt;/p&gt;

&lt;p&gt;And here’s what that hidden cost actually looked like:&lt;/p&gt;

&lt;p&gt;Reality Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;EC2 patching: 4 hours/month per instance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security updates: 2 hours/month&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitoring setup: 8 hours initially + ongoing maintenance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Capacity planning: 3 hours/month&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incident response: 6 hours/month average&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: ~25 hours/month on infrastructure babysitting&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a startup with only developers, staying deep in the EC2 world means accepting that you'll spend significant time on environment management and system administration.&lt;/strong&gt; That’s not why most of us became developers.&lt;/p&gt;

&lt;p&gt;As we prepared for our public launch, this operational overhead became a real concern. We needed to move fast, deploy reliably, and focus on building features—not troubleshooting environment inconsistencies.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All these operational headaches forced us to step back and ask ourselves a bigger question.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Moment Everything Clicked
&lt;/h2&gt;

&lt;p&gt;The breakthrough came when I realised we were solving the wrong problem. We weren't trying to manage servers—we were trying to run applications.&lt;/p&gt;

&lt;p&gt;That mental shift changed everything.&lt;/p&gt;

&lt;p&gt;Instead of asking "which server will this run on?", we started asking "what does this service actually need?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ECS Fargate Became Our Secret Weapon
&lt;/h2&gt;

&lt;p&gt;We considered three paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;ECS with EC2: More control, Spot instances, Graviton savings (but more operational overhead)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EKS: Full Kubernetes power and flexibility…but personally, as a developer, I find this path complex and better suited to teams with dedicated platform engineers. It felt like overkill for our small team&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ECS Fargate: Serverless containers (perfect for developer-heavy teams)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a team of 3 engineers with no dedicated infrastructure expertise, Fargate won because it eliminated the exact operational problems that were slowing us down:&lt;/p&gt;

&lt;h3&gt;
  
  
  Before: Environment Inconsistency
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;"This works fine in development, but staging has different Python versions."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We need to manually install these system packages on every server."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Production deployment failed because of a missing dependency."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  After: Container Consistency
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;"Build the container once, run it everywhere.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"All environments use the exact same container image.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Deployment is just pulling and running a container—no surprises."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It wasn’t just about technology. It was about giving our small team the freedom to build features without worrying about the plumbing underneath. For us, Fargate meant moving faster, deploying more reliably, and finally escaping the chaos of managing EC2 servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration: What We Wish We'd Known
&lt;/h2&gt;

&lt;p&gt;Even as someone deeply familiar with containers, these areas still tripped me up during migration. They’re easy to overlook—and that’s why I’m sharing them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Victory #1: The IAM Maze (And How to Navigate It)
&lt;/h3&gt;

&lt;p&gt;This tripped us up for days. ECS needs TWO different roles:&lt;/p&gt;

&lt;p&gt;Task Role: What your application can do&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execution Role: What ECS can do to run your application&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ecr:GetDownloadUrlForLayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"logs:CreateLogStream"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pro tip: Create the execution role first, then focus on task permissions. Don't mix them up like we did. I have explained in one of Youtube Shorts video &lt;a href="https://youtube.com/shorts/6-MxMB3E43U?si=FyfEr0_CLH8bJT0g" rel="noopener noreferrer"&gt;https://youtube.com/shorts/6-MxMB3E43U?si=FyfEr0_CLH8bJT0g&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Victory #2: Goodbye SSH, Hello Observability
&lt;/h3&gt;

&lt;p&gt;The hardest mental shift? No more SSH debugging. But this forced us to build better logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: Debug by SSH and printf
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_network_request&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# hope nothing breaks
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="c1"&gt;# After: Structured logging for containers
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;structlog&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;structlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_logger&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;process_network_request&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing_network_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# proper error handling and metrics
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Victory #3: The Security Groups Revelation
&lt;/h3&gt;

&lt;p&gt;EC2 security groups felt simple—one instance, one set of rules. But Fargate tasks need precise networking:&lt;/p&gt;

&lt;p&gt;Our mistake: Copying EC2 security group rules directly to Fargate tasks&lt;/p&gt;

&lt;p&gt;The fix: Each ECS service gets its own security group with minimal required access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Flask API: Only needs outbound to RDS and inbound from ALB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebSocket: Needs different port ranges and longer timeouts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Background jobs: Only outbound to external APIs&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Numbers That Matter
&lt;/h2&gt;

&lt;p&gt;Six months post-migration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (EC2)&lt;/th&gt;
&lt;th&gt;After (Fargate)&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Environment setup time&lt;/td&gt;
&lt;td&gt;4-6 hours&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;td&gt;-95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment time&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;td&gt;3 minutes&lt;/td&gt;
&lt;td&gt;-80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment failures&lt;/td&gt;
&lt;td&gt;1 in 5&lt;/td&gt;
&lt;td&gt;1 in 50&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Works on my machine" incidents&lt;/td&gt;
&lt;td&gt;3-4/month&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;-100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But the real win? We stopped being environment troubleshooters and became product builders again. With our public launch approaching, this consistency became invaluable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Microservices Question: When to Split, When to Keep Together
&lt;/h2&gt;

&lt;p&gt;Here's where most teams overthink it—and where your team structure should guide your decision.&lt;/p&gt;

&lt;p&gt;Our Flask app was already modular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/networks/*&lt;/code&gt; routes → &lt;a href="http://networks.py" rel="noopener noreferrer"&gt;networks.py&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/profile/*&lt;/code&gt; routes → &lt;a href="http://profile.py" rel="noopener noreferrer"&gt;profile.py&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebSocket handling → separate module&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We could have split these into separate ECS services immediately. But we didn't.&lt;/p&gt;

&lt;p&gt;Why we kept them together initially:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Shared authentication logic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Small team (3 engineers, no dedicated DevOps)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Approaching public launch (needed stability over optimization)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limited operational bandwidth for managing multiple services&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Containers gave us the flexibility to split later without the environment consistency problems we'd face with EC2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And there’s another reason containers were the right choice for us—even if we started monolithic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;With containers, you can right-size your compute per service as you grow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If one part of your app is lightweight, you can run it in a small container to save costs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If another part becomes resource-intensive, you can allocate more CPU, memory, or even switch to a larger container class—all without changing the rest of your system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This means you can optimise your AWS spend at a much more granular level than with monolithic EC2 instances.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That flexibility was a huge part of why we chose containers from day one. We knew we might not need microservices immediately—but when we did, we’d be ready to split workloads and optimize costs without rewriting everything from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Team-Size Reality Check
&lt;/h3&gt;

&lt;p&gt;If you have 1-3 developers doing everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Start with containers, but keep services together&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Split only when you have clear performance bottlenecks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prioritize simplicity over "best practices"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have 5+ engineers or dedicated platform team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Consider splitting services earlier&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You have bandwidth to manage multiple deployment pipelines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Microservices architecture becomes more viable&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have dedicated DevOps/Platform engineers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ECS on EC2 might be worth considering for cost optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can handle the operational complexity of managing instances&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes (EKS) becomes a viable option&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Our Splitting Strategy (For Small Teams)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Start monolithic in containers (easier migration)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitor and measure actual bottlenecks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Split services when you have clear scaling needs AND team bandwidth&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grow your operational maturity alongside your architecture complexity&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Signs it's time to split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One component consistently uses 80%+ CPU while others idle&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Different scaling patterns (API traffic vs background jobs)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Team growth (multiple people working on same codebase)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Unexpected Wins
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Environment Parity That Actually Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same container, different environments&lt;/span&gt;
&lt;span class="na"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;
  &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
&lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512&lt;/span&gt;  
  &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
  &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2048&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Feature Flags for Infrastructure
&lt;/h3&gt;

&lt;p&gt;Need to test a new background job? Deploy it as a separate ECS service with minimal resources. No risk to existing services.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cost Optimisation by Service
&lt;/h3&gt;

&lt;p&gt;We discovered our podcast audio processing was using 60% of our compute budget. Easy to optimise when you can see it clearly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with Monitoring Day One
&lt;/h3&gt;

&lt;p&gt;Don't wait until after migration. Set up CloudWatch Container Insights and structured logging immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embrace Infrastructure as Code Earlier (It's Easier Than You Think)
&lt;/h3&gt;

&lt;p&gt;We initially managed ECS through the console. Big mistake. CloudFormation templates made everything repeatable and reviewable.&lt;/p&gt;

&lt;p&gt;But here's what changed the game: AI-powered infrastructure tools.&lt;/p&gt;

&lt;p&gt;I wrote our entire CloudFormation stack using Amazon Q Developer CLI and ChatGPT. What used to require deep AWS expertise now takes basic prompting skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; q chat
&lt;span class="c"&gt;# Amazon Q Developer CLI in action&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Generate CloudFormation template for ECS Fargate with ALB, RDS, and auto-scaling"&lt;/span&gt;

&lt;span class="c"&gt;# Review, modify, and iterate&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="s2"&gt;"Add CloudWatch Container Insights and log retention policies"&lt;/span&gt;

&lt;span class="c"&gt;# ChatGPT for fine-tuning&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Fix this IAM policy - getting access denied on ECR pull"&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: 300+ lines of CloudFormation that would have taken me weeks to write manually, delivered in 2 hours.&lt;/p&gt;

&lt;p&gt;The new reality: If you can describe your infrastructure in plain English, AI can write the CloudFormation. The barrier to Infrastructure as Code has practically disappeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan for Secrets Management
&lt;/h3&gt;

&lt;p&gt;We moved secrets from EC2 environment variables to AWS Systems Manager Parameter Store. Should have done this from the start. We didn’t have any secrete which require rotation so choose simple option SSM parameter store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leverage AI Tools for Infrastructure as Code
&lt;/h3&gt;

&lt;p&gt;Here's a game-changer: GenAI has eliminated the "I don't know CloudFormation" excuse.&lt;/p&gt;

&lt;p&gt;I wrote our entire ECS infrastructure using Amazon Q Developer CLI. The process looked like this:&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;# Ask Amazon Q to generate CloudFormation template&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Create CloudFormation template for ECS Fargate service with ALB and RDS"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Add CloudWatch log groups and auto-scaling policies to this template"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What used to take days now takes hours. The AI handles the boilerplate, you focus on the business logic.&lt;/p&gt;

&lt;p&gt;Pro tip: Use AI tools as your pair programmer, not your replacement. They're incredible at generating infrastructure templates, but you still need to understand what you're deploying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;EC2 isn't wrong—it's just not optimised for modern application patterns or small development teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose Your Path Based on Your Team Reality:
&lt;/h3&gt;

&lt;p&gt;Pure Developer Team (1-5 people, no DevOps):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fargate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managed databases (RDS, not self-hosted)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Serverless where possible&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI-generated Infrastructure as Code (Amazon Q, ChatGPT for CloudFormation)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid EC2 (unless you want to manage environments manually)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixed Team (Developers + DevOps/Platform Engineers):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ECS on EC2 or EKS for more control&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spot instances and Graviton for optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More complex architectures become viable&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI-assisted infrastructure optimization and troubleshooting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fargate still valid for rapid iteration&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large Team (10+ engineers, dedicated platform team):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Full Kubernetes (EKS/GKE)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-cloud strategies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Complex microservices architectures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Advanced cost optimization techniques&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running a simple, monolithic app with predictable patterns and dedicated infrastructure expertise, EC2 might be perfect. But if you're building a product that needs to move fast, deploy reliably, and maintain consistency across environments while your developers focus on product development, containers will eventually become inevitable.&lt;/p&gt;

&lt;p&gt;The question isn't whether to migrate—it's when, and what path matches your team's strengths and timeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ready to Start Your Migration? (The AI-Powered Way)
&lt;/h3&gt;

&lt;p&gt;The GenAI advantage: What used to require deep AWS expertise now needs basic prompting skills.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Audit your current EC2 usage (AWS Cost Explorer is your friend)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identify your biggest pain points (deployment time? scaling? costs?)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use AI to generate your infrastructure templates:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Amazon Q Developer CLI examples&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="s2"&gt;"Create ECS Fargate service template for my Flask app"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Add Application Load Balancer with HTTPS certificate"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="s2"&gt;"Configure auto-scaling based on CPU utilization"&lt;/span&gt;

&lt;span class="c"&gt;# ChatGPT for troubleshooting&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"My ECS task is failing with this error: [paste logs]"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Optimize this CloudFormation template for cost efficiency"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start with one service (pick the most isolated one)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Measure everything (before and after metrics)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Iterate and expand (let AI handle the infrastructure complexity)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The old excuse: "I don't know CloudFormation well enough" The new reality: "I can describe what I want in plain English"&lt;/p&gt;

&lt;p&gt;The best migration is the one you don't have to do twice—and AI ensures you get it right the first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Container Migration Cheat Sheet
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with EC2 if you must—but design your code so you can migrate later.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know your team’s capacity.&lt;/strong&gt; Small teams should prioritise simplicity and managed services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fargate is perfect for dev-heavy teams without dedicated DevOps.&lt;/strong&gt; It trades some control for massive operational relief.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ECS on EC2 offers cost savings—but requires infra expertise.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containerisation solves environment drift.&lt;/strong&gt; “It works on my machine” becomes a thing of the past.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don’t rush into Microservices.&lt;/strong&gt; Keep services together initially if your team is small.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containers let you right-size resources per service.&lt;/strong&gt; Small services can save money, while heavy services can scale independently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IAM roles in ECS are easy to confuse.&lt;/strong&gt; Separate your task role from your execution role.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ditch SSH for observability.&lt;/strong&gt; Logging and monitoring are non-negotiable in containerized workloads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invest in Infrastructure as Code early.&lt;/strong&gt; Use AI tools like Amazon Q or ChatGPT to help generate your templates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure everything before and after migration.&lt;/strong&gt; You can’t improve what you can’t measure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to discuss your EC2 to containers migration? I'd love to hear about your experience and challenges. Connect with me on LinkedIn/Twitter or drop a comment below.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>container</category>
      <category>aws</category>
      <category>ec2</category>
    </item>
    <item>
      <title>The Angular Error That Kept Scratching My Head: NG02100</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Fri, 27 Jun 2025 01:12:29 +0000</pubDate>
      <link>https://forem.com/playfulprogramming-angular/the-angular-error-that-kept-scratching-my-head-ng02100-4obg</link>
      <guid>https://forem.com/playfulprogramming-angular/the-angular-error-that-kept-scratching-my-head-ng02100-4obg</guid>
      <description>&lt;p&gt;Hello Devs,&lt;/p&gt;

&lt;p&gt;You know those moments where everything compiles, no warnings pop up, the UI mostly works… but then something just quietly breaks?&lt;/p&gt;

&lt;p&gt;This was one of those moments.&lt;/p&gt;

&lt;p&gt;I wasn’t refactoring some ancient service or tweaking a low-level renderer. I was building a simple notification drawer — and somehow, Angular's &lt;code&gt;NG02100&lt;/code&gt; error made it feel like the app was haunted.&lt;/p&gt;

&lt;h2&gt;
  
  
  When “Just a Template Change” Isn’t
&lt;/h2&gt;

&lt;p&gt;I was working on our notification drawer — a simple list showing who did what, when. The API was returning the usual data: message, title, timestamp. Nothing out of the ordinary.&lt;/p&gt;

&lt;p&gt;In the template, I rendered the notification time like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"notification-time"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ notification?.cd | date: 'MMM d, y h:mm a' }}
&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. I merged it. Life moved on.&lt;/p&gt;

&lt;p&gt;Until it didn’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Crash That Made No Sense
&lt;/h2&gt;

&lt;p&gt;One morning, I refreshed the UI. The first few notifications loaded. Then — out of nowhere — Angular crashed.&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%2Futuyysy7zjozuc6a96dg.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%2Futuyysy7zjozuc6a96dg.png" alt="it worked yesterday" width="800" height="851"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes it broke after the third item, sometimes after the fifth. It wasn’t consistent. But the crash was real, and the console wasn’t much help:&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="nx"&gt;ERROR&lt;/span&gt; &lt;span class="nx"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NG02100&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nf"&gt;qn &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1782481)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nx"&gt;Me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1784244)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;H2 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1923346)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Y2 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1924135)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nf"&gt;pp &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:556184)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;L0 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1865117)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;ZC &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1875545)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;X0 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1876956)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;Hb &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1876779)&lt;/span&gt;
    &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;Jm &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:4200/main.js:1:1876711)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mention of the template line. No stack trace pointing to my component. Just &lt;code&gt;NG02100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I hadn’t seen this one before. And I write a lot of Angular.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging the Wrong Places
&lt;/h2&gt;

&lt;p&gt;Like most devs, I assumed something logical was broken. So I started removing things.&lt;/p&gt;

&lt;p&gt;I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Wrapping fields in &lt;code&gt;*ngIf&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adding &lt;code&gt;?.&lt;/code&gt; everywhere&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logging the API response&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filtering out &lt;code&gt;null&lt;/code&gt; and incomplete records&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removing pipes, anchor tags, images, even entire rows&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At one point I replaced the whole template with just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;{{ notification | json }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still crashing.&lt;/p&gt;

&lt;p&gt;The error pointed to the line in my component where I assigned the response:&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And yet the first few notifications rendered fine. That made it worse — it felt like the problem was hiding somewhere deep in the data, waiting to ambush me at random.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough
&lt;/h2&gt;

&lt;p&gt;After hours of slicing code, I dumped a single notification to the console and noticed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;cd&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ago&lt;/span&gt;&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That used to be:&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="nl"&gt;"cd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-06-06T12:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what changed?&lt;/p&gt;

&lt;p&gt;The backend team had switched from sending ISO timestamps to human-readable time strings.&lt;/p&gt;

&lt;p&gt;Totally reasonable — but my &lt;code&gt;date&lt;/code&gt; pipe didn’t agree.&lt;/p&gt;

&lt;p&gt;Angular was trying to parse &lt;code&gt;"9 mins ago"&lt;/code&gt; with the &lt;code&gt;date&lt;/code&gt; pipe. Naturally, it failed. But instead of throwing a descriptive error, it threw &lt;code&gt;NG02100&lt;/code&gt;, which just means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Something in the template broke during binding. Good luck finding it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Fix Was Simple (But Finding It Wasn't)
&lt;/h2&gt;

&lt;p&gt;I removed the &lt;code&gt;date&lt;/code&gt; pipe entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"notification-time"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ notification?.cd }}
&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just like that — everything worked again.&lt;/p&gt;

&lt;p&gt;It took two hours to track down a single pipe that was assuming a valid date. A change that didn't cause TypeScript to fail. A runtime-only issue that crashed without context.&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%2Ftqjlroq14lh8nursd1on.gif" 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%2Ftqjlroq14lh8nursd1on.gif" alt="Finally worked" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from the Trap
&lt;/h2&gt;

&lt;p&gt;Looking back, this one line taught me more than I expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NG02100 is a template binding failure&lt;/strong&gt;, not a logic error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pipes like &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, or &lt;code&gt;async&lt;/code&gt; are easy to break if the data isn’t shaped exactly as expected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When errors seem random in a loop (&lt;code&gt;*ngFor&lt;/code&gt;), it’s usually &lt;strong&gt;one bad record&lt;/strong&gt; in the list.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Defensive programming in templates is just as important as in your components.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I’d Do Differently Now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Validate data types before using them in the template.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Guard pipes behind helper functions if the format can vary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use conditional formatting:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And finally, when I see &lt;code&gt;NG02100&lt;/code&gt; again, I’ll know where to look first — the template, not the logic&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This wasn’t a tricky bug in hindsight. But it was just invisible enough, just misleading enough, to waste hours of time.&lt;/p&gt;

&lt;p&gt;Sometimes the problem isn’t what changed — it’s what &lt;strong&gt;you assumed would never change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you ever see &lt;code&gt;NG02100&lt;/code&gt;, don’t start with your TypeScript.&lt;/p&gt;

&lt;p&gt;Start with your templates.&lt;/p&gt;

&lt;p&gt;And then check your pipes.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>javascript</category>
      <category>learning</category>
    </item>
    <item>
      <title>Why your ECS tasks aren’t scaling</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Sun, 15 Jun 2025 14:59:18 +0000</pubDate>
      <link>https://forem.com/aws-builders/why-your-ecs-tasks-arent-scaling-5c04</link>
      <guid>https://forem.com/aws-builders/why-your-ecs-tasks-arent-scaling-5c04</guid>
      <description>&lt;p&gt;We had auto scaling set. Alarms configured. Metrics wired. And yet—502s.&lt;/p&gt;

&lt;p&gt;That was the story every month-end in our GIS image processing app. A spike in usage from ops teams. Annotation tools slowing down. And the infamous error that no one wants to debug under pressure.&lt;/p&gt;

&lt;p&gt;The ECS setup wasn’t new—built by the previous team—but now it was on us: developers and DevOps engineers trying to make sense of why scaling wasn’t saving us.&lt;/p&gt;

&lt;p&gt;We did what most teams would do. We scaled the ECS service. Added more tasks. And for a while, it worked. Until it didn’t.&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%2F5r7mjv1ck3vwjgao6o47.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5r7mjv1ck3vwjgao6o47.jpeg" width="507" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This blog isn’t just about &lt;strong&gt;what CAS is&lt;/strong&gt;—there are plenty of docs for that. This is about &lt;strong&gt;why you might miss it&lt;/strong&gt;, how we almost did, and what real-world capacity alignment actually looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most Builders Miss This: Task Scaling Isn’t Enough
&lt;/h2&gt;

&lt;p&gt;When you configure ECS Service Auto Scaling (like scaling from 2 to 10 tasks based on CPU &amp;gt; 50%), ECS will try to place new tasks.&lt;/p&gt;

&lt;p&gt;But here’s the catch:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re using &lt;strong&gt;EC2 launch type&lt;/strong&gt;, ECS needs &lt;strong&gt;available capacity&lt;/strong&gt; on the cluster to actually place those tasks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No CPU or memory available? The tasks stay stuck in &lt;code&gt;PENDING&lt;/code&gt;. And it’s silent unless you're watching.&lt;/p&gt;

&lt;p&gt;Here’s where &lt;strong&gt;ECS Cluster Auto Scaling (CAS)&lt;/strong&gt; enters the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Past Pain: Month-End GIS Workloads That Failed to Scale
&lt;/h2&gt;

&lt;p&gt;In a previous role, we managed an internal image processing tool that rendered GIS data and allowed operations teams to annotate high-resolution maps. It wasn’t a real-time app — but it was heavy. And during critical windows like month-end or year-end closures, load would spike massively.&lt;/p&gt;

&lt;p&gt;The app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Generated map tiles on the fly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handled concurrent uploads and annotations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Involved CPU-heavy image processing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We assumed ECS auto scaling would “just work.” But then came 502s.&lt;/p&gt;

&lt;p&gt;Naturally, we began by debugging the app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Checked RDS performance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tuned Apache settings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reproduced failures with same payloads&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing helped. The mystery deepened.&lt;/p&gt;

&lt;p&gt;Until we noticed this: tasks were stuck in &lt;code&gt;PENDING&lt;/code&gt;, but &lt;strong&gt;CPU and memory metrics looked fine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s when we connected the dots. We had scaling at the &lt;strong&gt;task level&lt;/strong&gt;, but the &lt;strong&gt;infrastructure wasn’t scaling with it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was like hiring more workers without giving them desks.&lt;/strong&gt; We were adding more containers, but the underlying compute had no room to host them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Estimate Capacity Like a Developer
&lt;/h2&gt;

&lt;p&gt;Let’s say you're running a Flask or FastAPI app on ECS. The app handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;10–12 API calls per user action&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each API call does a DB lookup + image transform&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spikes happen during end-of-day or batch usage&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How do you estimate how many ECS tasks you need?
&lt;/h3&gt;

&lt;p&gt;Here’s a &lt;strong&gt;developer-first method&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Step 1: Understand the API behaviour&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What is the &lt;strong&gt;average latency&lt;/strong&gt; of a single API call? (e.g. 500ms)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are the calls &lt;strong&gt;CPU or memory bound&lt;/strong&gt;? (CloudWatch / APM tools)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What’s the &lt;strong&gt;max concurrency&lt;/strong&gt;? (e.g. 100 users x 10 calls = 1,000)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If each ECS task can handle ~10 concurrent API calls → you need &lt;strong&gt;~100 tasks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Know Your Task Size&lt;/p&gt;

&lt;p&gt;If task = 0.25 vCPU, 512 MB and EC2 = 2 vCPU, 8 GB → host ~8 tasks per EC2&lt;/p&gt;

&lt;p&gt;100 tasks → ~13 EC2s&lt;/p&gt;

&lt;p&gt;Step 3: Monitor Key Metrics&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CPUReservation&lt;/code&gt; and &lt;code&gt;MemoryReservation&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PendingTaskCount&lt;/code&gt; (cluster)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ECS &lt;code&gt;ManagedScaling&lt;/code&gt; logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;App logs for 502s, slow endpoints, queuing behavior&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Step 4: Set Scaling Policies&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Task scaling: CPU &amp;gt; 50%&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CAS scaling: set &lt;code&gt;targetCapacity = 80%&lt;/code&gt; for buffer&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How ECS Cluster Auto Scaling Actually Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It’s Not Magic — It’s Math
&lt;/h3&gt;

&lt;p&gt;When ECS needs to launch new tasks but can't due to resource shortage, it uses a &lt;strong&gt;Capacity Provider&lt;/strong&gt; with a formula like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;desired = ceil((needed capacity) / (instance capacity)) * target capacity %
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s say you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pending Tasks&lt;/strong&gt;: 4 tasks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Each Task Needs&lt;/strong&gt;: 0.5 vCPU and 1 GB RAM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;EC2 Type&lt;/strong&gt;: &lt;code&gt;t4g.medium&lt;/code&gt; (2 vCPU, 4 GB RAM)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Target Capacity&lt;/strong&gt;: 100% (binpack strategy)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step-by-step:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Total Needed Capacity&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* 2 vCPU (0.5 x 4)

* 4 GB RAM (1 x 4)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Per Instance Capacity&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* 2 vCPU and 4 GB RAM per `t4g.medium`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Divide &amp;amp; Ceil&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* CPU: 2 / 2 = 1

* Memory: 4 / 4 = 1

* Take the **max of the two** = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apply Target Capacity %&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* At 100% target, no buffer → `desired = 1` EC2 instance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So CAS would scale &lt;strong&gt;one t4g.medium&lt;/strong&gt; to place those four tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target capacity&lt;/strong&gt; lets you control buffer: set to 100% for binpack-style efficiency, or 80% for headroom.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Concepts from ECS CAS Internals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ECS checks task placement every 15 seconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If it can’t place tasks, they go into the &lt;strong&gt;provisioning state&lt;/strong&gt; (not failed)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CAS calculates how many EC2s are needed based on task resource demand&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Up to 100 tasks can be in provisioning per cluster&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provisioning timeout is 10–30 minutes before task is stopped&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Daemon vs Non-Daemon Tasks: What Matters for Scaling
&lt;/h2&gt;

&lt;p&gt;Daemon Task&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scheduled to run on &lt;strong&gt;every EC2 instance&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Used for agents, log forwarders, metrics collectors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ECS &lt;strong&gt;ignores these&lt;/strong&gt; when calculating scale-out/scale-in&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Non-Daemon Task&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your real app workloads (Flask, Socket, Workers)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;These determine whether EC2s are needed or idle&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How ECS Decides How Many EC2s to Run
&lt;/h2&gt;

&lt;p&gt;Let’s say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;N&lt;/code&gt; = current EC2s&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;M&lt;/code&gt; = desired EC2s (CAS output)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No pending tasks, all EC2s used&lt;/td&gt;
&lt;td&gt;M = N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pending tasks present&lt;/td&gt;
&lt;td&gt;M &amp;gt; N (scale out)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle EC2s (only daemon tasks)&lt;/td&gt;
&lt;td&gt;M &amp;lt; N (scale in)&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.amazonaws.com%2Fuploads%2Farticles%2Fycfu0a5i9bdv4mjxg72v.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%2Fycfu0a5i9bdv4mjxg72v.png" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ECS in Real Life: Before and After CAS
&lt;/h2&gt;

&lt;p&gt;Once we added &lt;strong&gt;CAS&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We linked services to a capacity provider&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enabled managed scaling (target = 100%)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Switched placement to &lt;code&gt;binpack&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, tasks scaled. And so did the infra. No more 502s.&lt;/p&gt;

&lt;h2&gt;
  
  
  What If You're Launching a New App and Don’t Know the Load Yet?
&lt;/h2&gt;

&lt;p&gt;Start lean. Scale for learnings.&lt;/p&gt;

&lt;p&gt;When launching something new—like we are with NuShift—it’s often unclear what kind of user load or traffic patterns to expect. In such cases, make decisions based on expected concurrency, your framework’s behaviour, and instance characteristics.&lt;/p&gt;

&lt;p&gt;Here are some tips to guide early capacity planning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Estimate concurrency&lt;/strong&gt;: If you expect 50–100 concurrent users, and each user triggers multiple API calls, try to estimate peak call concurrency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Understand your app behaviour&lt;/strong&gt;: Flask or FastAPI-based apps usually work well with 0.25 vCPU and 512MB, especially if I/O bound (e.g., API calls, DB reads). If your app does image processing or CPU-intensive work, start with 0.5 vCPU.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose your EC2 wisely&lt;/strong&gt;: We use &lt;code&gt;t4g.medium&lt;/code&gt; (2 vCPU, 4GB RAM) for its cost-efficiency and support for multiple small tasks (6–8 per instance).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor early patterns&lt;/strong&gt;: Let metrics shape your scaling curve—track &lt;code&gt;CPUUtilisation&lt;/code&gt;, &lt;code&gt;MemoryUtilisation&lt;/code&gt;, and task startup times.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example initial config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Flask API: 1–3 tasks (0.25 vCPU, 512 MB)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebSocket: 1–2 tasks (depends on socket concurrency)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EC2: t4g.medium in an ASG with ECS capacity provider&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CAS: enabled with 80% targetCapacity for buffer&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use New Relic, CloudWatch, or X-Ray to track CPU, memory, latency, and pending counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Scaling your application is easy to talk about. But infrastructure scaling is where things quietly break.&lt;/p&gt;

&lt;p&gt;If you’re only watching task counts and CPU graphs, you might miss deeper issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PENDING tasks with nowhere to run&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EC2s running agents, not apps&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cold starts caused by infra lag&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Auto scaling isn’t just about adding containers—it’s about giving them somewhere to live&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/containers/deep-dive-on-amazon-ecs-cluster-auto-scaling/" rel="noopener noreferrer"&gt;Deep Dive on Amazon ECS Cluster Auto Scaling (Official AWS Blog)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement-strategies.html" rel="noopener noreferrer"&gt;ECS Task Placement Strategies&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ecs</category>
      <category>ec2</category>
      <category>aws</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Behind the Scenes of AWS Community Days Bengaluru 2025</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Fri, 30 May 2025 02:01:49 +0000</pubDate>
      <link>https://forem.com/aws-builders/behind-the-scenes-of-aws-community-days-bengaluru-2025-21oj</link>
      <guid>https://forem.com/aws-builders/behind-the-scenes-of-aws-community-days-bengaluru-2025-21oj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw267644ysf601hylidlh.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%2Fw267644ysf601hylidlh.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The lights were bright, the crowd buzzing, and everything looked perfect — but just hours before the show began, I stood still backstage, feeling a familiar knot in my stomach. Not out of fear, but from the weight of a moment that had been years — maybe a decade — in the making. I didn’t fully understand why it meant so much, not yet.&lt;/p&gt;

&lt;p&gt;That clarity came later. Like all good stories — this one started with a spark, but the meaning would only unfold with time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Great teams aren’t built on agreement — they’re built on shared purpose.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one line sums up our journey toward AWS Community Day Bengaluru 2025.&lt;/p&gt;

&lt;p&gt;What started in December as a simple conversation soon turned into months of collaboration, debates, design chaos, speaker curation, and pure execution energy.  We came in with diverse roles — some of us focusing on speakers, some on partnerships, others on content, marketing, logistics, or tech. We didn’t always agree, but we never lost sight of the goal: delivering a memorable experience for the community.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Start: Vision to Action
&lt;/h3&gt;

&lt;p&gt;The vision was simple — make this the most meaningful and elevated version of ACD Bengaluru yet. Bigger in scale, deeper in content, richer in community vibes. But turning that vision into action meant asking tough questions and making bold decisions.&lt;/p&gt;

&lt;p&gt;We explored venue options, discussed format changes, and debated everything — from the layout of tracks to the type of workshops we wanted to host. It all started with a WhatsApp group and a few Google Meet calls. Weekend working sessions were filled with passionate arguments — and what's more magical is that the entire team never met in person, not even once. Yet, the synergy and symphony were real, because our goal was always clear and shared.&lt;/p&gt;

&lt;p&gt;This was my first time organizing an event at such a large scale. Last year, I had organized &lt;strong&gt;Mautic Conference India&lt;/strong&gt; in Pune — a proud moment with about 100 participants &lt;a href="https://www.internetkatta.com/from-fan-to-organizer-my-incredible-journey-with-mautic-conference-india" rel="noopener noreferrer"&gt;shared here&lt;/a&gt;. And I had volunteered for &lt;strong&gt;ACD BLR 2024&lt;/strong&gt;, which was hosted at the Amazon office. But stepping up to co-lead the effort at a grand venue like &lt;strong&gt;Conrad Bengaluru&lt;/strong&gt; was a completely different level. The stakes were higher, the pressure was real — but so was the passion.&lt;/p&gt;

&lt;p&gt;That moment brought back an old, bittersweet memory. Back in engineering college, I had one dream: to become the &lt;strong&gt;CSR (Cultural Representative)&lt;/strong&gt; — the person who would run the college fest, rally the teams, and own the spotlight. But college politics had other plans. I never got that role. That unfulfilled dream stayed tucked away — quietly lingering.&lt;/p&gt;

&lt;p&gt;Yet, life has its own timeline. Organizing ACD BLR 2025, in front of hundreds of people, with a team I admire — it felt like that dream found its way back to me, not in the place I first imagined, but in a space where I was truly meant to lead, contribute, and shine. A heartfelt thanks to &lt;strong&gt;Bhuvana&lt;/strong&gt;, &lt;strong&gt;Jones&lt;/strong&gt;, and &lt;strong&gt;Ayyanar&lt;/strong&gt; for trusting and believing in me with this opportunity — your confidence made all the difference.&lt;/p&gt;

&lt;p&gt;Sometimes, you don’t get to live that dream in the place you imagined. But you get something better — a stage that truly values your intent, a platform where your efforts shine, and a team that believes in your vision. &lt;strong&gt;This was that moment for me.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Build-Up: Decisions, Deadlines, and Dilemmas
&lt;/h3&gt;

&lt;p&gt;As we moved into execution, the real challenges began. From finalising keynote speakers to rolling out creative ticket sale campaigns, each step required hustle, agility, and relentless problem-solving. We tested virtual photo booths, tweaked LinkedIn promos, crafted FOMO posts, created session posters, curated cue cards for emcees, and pulled off a social media blitz powered by our volunteers.&lt;/p&gt;

&lt;p&gt;We also kickstarted two powerful community-led initiatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blogathon&lt;/strong&gt;: A platform for community members to share their AWS learnings, stories, and hands-on experiments. It amplified new voices and celebrated the diverse experiences across our builder ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Voice of AWS UG Bengaluru&lt;/strong&gt;: A new storytelling series where we spotlight the journeys, challenges, and achievements of our local AWS User Group members — giving a voice to the builders who power our community.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two moments stood out for me as serendipitous reminders of how everything comes full circle:&lt;/p&gt;

&lt;p&gt;The first was when my engineering college friend &lt;strong&gt;Sagar&lt;/strong&gt;, who now runs an event management company called StudioB3, showed up at ACD. We hadn’t met in over 12 years. He’s been doing events for more than a decade — something I had completely forgotten. But someone once told me, &lt;em&gt;"When your time comes, the right people will show up to help you."&lt;/em&gt; Reconnecting with Sagar at this moment felt like that kind of magic.&lt;/p&gt;

&lt;p&gt;The second was when &lt;strong&gt;Talvinder Singh&lt;/strong&gt;, founder of &lt;a href="https://zop.dev/" rel="noopener noreferrer"&gt;zop.dev&lt;/a&gt;, reached out via my official email regarding AWS cloud services. I could’ve replied formally, but instead, I sent him a message from my personal email — pitching the idea of sponsorship. And guess what? Talvinder turned out to be a former client from my time at Sodel Solutions. A full-circle moment, reminding me that when you build good relationships, people remember you.&lt;/p&gt;

&lt;p&gt;Both moments reminded me that success is never solo — it's shaped by people, timing, and the relationships you nurture along the way.&lt;/p&gt;

&lt;p&gt;It wasn’t perfect. And that’s what made it special.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Moment to Recharge Before Show Time
&lt;/h3&gt;

&lt;p&gt;Just before the final days of prep for ACD BLR 2025, a last-minute yet incredibly meaningful opportunity came our way — the inauguration of the &lt;strong&gt;first AWS Cloud Club in Bengaluru&lt;/strong&gt; at &lt;strong&gt;Ramaiah Institute of Technology&lt;/strong&gt;. The college had invited AWS UG leaders to attend the event alongside our keynote speaker &lt;strong&gt;Vivek Raja P S&lt;/strong&gt;. Though &lt;strong&gt;Jason&lt;/strong&gt;, Senior Program Manager of AWS Community Builders, couldn't join due to unforeseen reasons, his spirit was very much present.&lt;/p&gt;

&lt;p&gt;Spending half a day there, celebrating the new generation of cloud enthusiasts, was a refreshing break — and a reminder of &lt;em&gt;why&lt;/em&gt; we do this in the first place. Right after the ceremony, we were back in execution mode — refining ACD details, tying up loose ends, and getting ready to welcome hundreds of builders to Bengaluru.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Day(s): Goosebumps, High-Fives, and Happy Faces
&lt;/h3&gt;

&lt;p&gt;This time, I was doing something I’d never done before — speaking in front of the entire audience in the combined auditorium. Over the years, I’ve spoken in tracks and breakout rooms, but this was different. Bigger. A little scary. And deeply exciting. What made it even more special was sharing that stage with &lt;strong&gt;Vivek Raja P S&lt;/strong&gt;, someone I deeply respect.&lt;/p&gt;

&lt;p&gt;I wasn’t prepared the way I usually like to be — there were just too many moving parts in the days leading up to the event. But I reminded myself: I’ve been on stage more than 30 times. I knew I could pull through.&lt;/p&gt;

&lt;p&gt;We tried something different — a conversational-style talk instead of a standard slide-heavy presentation. And to our surprise, many people came up to us afterward saying it helped them understand complex topics more easily. That made us smile. And just like that, the nervousness faded.&lt;/p&gt;

&lt;p&gt;I didn’t know how the moment would unfold. Maybe we’d make mistakes. Maybe something wouldn’t go as planned. But I knew this for sure — we had built this event with heart.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For every learner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every builder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every dreamer walking into ACD Bengaluru.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Walking into the venue, seeing everything come together — the tracks, booths, branding, swag, and smiles — was an emotional moment for many of us. The reactions from attendees, speakers, and sponsors reminded us that all the hard work had meaning.&lt;/p&gt;

&lt;p&gt;Whether it was a packed Exhibition Lounge, a selfie with Jason (yes, that happened), or just the quiet moment of seeing a community member feel seen and celebrated — those were the wins we carried home.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Made It Work?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Diverse opinions, united by purpose&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Community-first thinking&lt;/strong&gt; in every decision&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A willingness to do the grunt work&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Volunteers who went above and beyond&lt;/strong&gt;, without expecting spotlight&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Faces Behind the Magic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ansh, Isha, and Yashavi&lt;/strong&gt; – The spotlight of the event as emcees. Their energy, clarity, and confidence brought the sessions to life and kept the momentum flowing throughout the day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chetan's wife&lt;/strong&gt; – A silent force of support who chipped in without hesitation and helped the team stay grounded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Yogesh and his friend -&lt;/strong&gt; Our little and young champions who contributed like squirrel did in Ramayana.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mehnaz &amp;amp; Nabhanyua&lt;/strong&gt; – Helped manage event-day logistics, always ready to lend a hand where needed. Their roles may have been short, but their impact was lasting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bhuvana&lt;/strong&gt; – The soul of AWS UG Bengaluru, her calm energy kept everything grounded. She’s the glue that held people and priorities together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Jones&lt;/strong&gt; – The finance ministry and collaboration expert of our team. Always patient, always composed — he ensured everything behind the scenes ran with clarity and calm. Whether it was budget planning or community outreach, Jones brought structure, insight, and an unwavering willingness to help.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vivek Raja&lt;/strong&gt; – The ML brain with a builder’s heart. Whether it was on-stage storytelling or behind-the-scenes mentoring, Vivek added thought leadership with humility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Srushith&lt;/strong&gt; – The brain behind so many tactics that made this event impactful. From growth strategies to last-mile execution, his input often shaped the outcome — even when he stayed behind the curtain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ayyanar&lt;/strong&gt; – Deep tech wizard. His AI/ML depth and thoughtful insights shaped the content backbone of the event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Poobalan&lt;/strong&gt; – He was always open to take up any challenge that came his way. From building the website — which was beautifully done — to managing social media seamlessly on event day, his openness and commitment truly stood out. A community strategist with a heart of gold and a can-do spirit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logesh&lt;/strong&gt; – A shadow for me in all things design, Logesh quietly helped shape the visual tone of the event. But on the event day, he stepped up and made sure everything ran smoothly on the ground — from handling AV to ensuring speaker content displayed right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chetan&lt;/strong&gt; – The quiet executor. If something needed doing, it was already done. No noise, just results.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Harsha&lt;/strong&gt; – Our perfectionist — always making sure our content and write-ups were polished and error-free. Unfortunately, he couldn’t be with us on event day due to a personal commitment, but his contributions in the lead-up were truly valuable and deeply appreciated. A silent force who made a loud impact. smile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ramya&lt;/strong&gt; – The swags master. From ideating to coordinating distribution, she ensured every attendee felt valued with thoughtful takeaways.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vijaya Nirmala&lt;/strong&gt; – Supporting us from far away outside India, she showed that distance doesn’t matter when the intent is strong. Always present in spirit and helping wherever possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Koti&lt;/strong&gt; – The voice of calm in chaos. He’s a true community leader who brings in diverse insights from his experience running PyCon India — always calm in chaos. Whenever things went haywire, Koti was there with a plan and a laugh.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ayyanar &amp;amp; Srushith&lt;/strong&gt; – The creative minds behind the AI speaker invention visuals that caught everyone’s attention. Their imaginative execution and attention to detail brought an extra layer of innovation to the event's design language.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks also to the &lt;a href="https://konfhub.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Konfhub&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;team&lt;/strong&gt;, especially &lt;strong&gt;Hari&lt;/strong&gt; and &lt;strong&gt;Ganesh&lt;/strong&gt;, for their incredible support in making this event a smooth experience — from handling ticketing operations seamlessly to sharing valuable insights whenever we hit a snag. Their behind-the-scenes help played a big part in the overall experience.&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%2F7003ge97y3tbbiqqdair.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%2F7003ge97y3tbbiqqdair.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Life Behind the Curtain
&lt;/h3&gt;

&lt;p&gt;They say,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A smooth sea never made a skilled sailor."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And ACD BLR 2025 wasn’t just a journey of coordination — it was a test of balance, resilience, and purpose.&lt;/p&gt;

&lt;p&gt;In the midst of all the planning and execution, life threw its own challenges. I was navigating some unexpected health issues — a gastrointestinal scare that pushed me to completely rethink my lifestyle and embrace fitness and clean eating. Just when I was finding a rhythm, my wife had to undergo a sudden surgery. Managing doctor visits, home responsibilities, a full-time job, and community commitments — it felt like everything hit at once.&lt;/p&gt;

&lt;p&gt;But through it all, my family stood like a rock. They never once said, “Pause the community work.” They encouraged it. They believed in it. They believed in me. And that quiet support — the kind that doesn’t need applause — is what carried me through the chaos.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Personal Note
&lt;/h3&gt;

&lt;p&gt;As someone who’s been part of many community events, ACD BLR 2025 felt different. It was intense, emotional, and fulfilling. It proved once again that when builders, doers, and dreamers come together with clarity of purpose — magic happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  To the Team:
&lt;/h3&gt;

&lt;p&gt;Thank you. You weren’t just volunteers — you were visionaries, warriors, and glue that held it all together. This one’s for you.&lt;/p&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/i6WMuXA4wMM"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;h3&gt;
  
  
  References :
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AWS UG BLR meet up link to join - &lt;a href="http://meetup.com/awsugblr" rel="noopener noreferrer"&gt;http://meetup.com/awsugblr&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS Community Day Bengaluru Blogathon awesome stories - &lt;a href="https://acdblr-blogathon.devpost.com/project-gallery" rel="noopener noreferrer"&gt;https://acdblr-blogathon.devpost.com/project-gallery&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://konfhub.com/" rel="noopener noreferrer"&gt;Konfhub&lt;/a&gt; - our ticketing platform&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>community</category>
      <category>story</category>
      <category>acd</category>
    </item>
    <item>
      <title>The Angular Standalone Component Gotcha I Didn’t See Coming</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Mon, 28 Apr 2025 11:34:39 +0000</pubDate>
      <link>https://forem.com/playfulprogramming-angular/the-angular-standalone-component-gotcha-i-didnt-see-coming-3571</link>
      <guid>https://forem.com/playfulprogramming-angular/the-angular-standalone-component-gotcha-i-didnt-see-coming-3571</guid>
      <description>&lt;p&gt;Hello Devs,&lt;/p&gt;

&lt;p&gt;You know that moment when everything looks fine — no errors, no warnings — but the UI just... doesn’t do what it’s supposed to?&lt;br&gt;&lt;br&gt;
That’s how this story begins.&lt;/p&gt;

&lt;p&gt;But let me add some background first...&lt;/p&gt;


&lt;h2&gt;
  
  
  🧳 Returning to Angular After 5 Years
&lt;/h2&gt;

&lt;p&gt;I recently jumped back into Angular after almost &lt;strong&gt;five years&lt;/strong&gt; away.&lt;/p&gt;

&lt;p&gt;Things I used to know by heart?&lt;br&gt;&lt;br&gt;
Gone or evolved.&lt;/p&gt;

&lt;p&gt;There’s &lt;code&gt;standalone: true&lt;/code&gt; now. Modules are optional. Components feel more like islands.&lt;br&gt;&lt;br&gt;
It’s Angular — but... different.&lt;/p&gt;

&lt;p&gt;So when I decided to build a simple standalone navbar component with a dropdown filter, I felt ready to dive in.&lt;/p&gt;

&lt;p&gt;After all, how hard could &lt;code&gt;*ngFor&lt;/code&gt; be?&lt;/p&gt;

&lt;p&gt;Turns out, harder than I expected — if you're doing it blindly.&lt;/p&gt;


&lt;h2&gt;
  
  
  🚧 The Setup
&lt;/h2&gt;

&lt;p&gt;I was building a &lt;code&gt;home-navbar&lt;/code&gt; component — a simple filter with values like “Latest”, “Trending”, and “Following”.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;home-navbar.component.ts&lt;/code&gt;, I had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;filterOptions &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'latest'&lt;/span&gt;, &lt;span class="s1"&gt;'trending'&lt;/span&gt;, &lt;span class="s1"&gt;'following'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
  &amp;lt;li &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;ngFor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"let option of filterOptions"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;{{&lt;/span&gt; option &lt;span class="o"&gt;}}&lt;/span&gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So clean. So elegant.&lt;br&gt;&lt;br&gt;
So... &lt;strong&gt;not working&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  😵‍💫 Debugging the Invisible
&lt;/h2&gt;

&lt;p&gt;No errors. No warnings. Just an empty &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
The kind of bug where Angular doesn’t even bother complaining — it just shrugs.&lt;/p&gt;

&lt;p&gt;Here’s what I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;code&gt;console.log(filterOptions)&lt;/code&gt; — the array was there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;code&gt;{{ filterOptions }}&lt;/code&gt; in the template — rendered just fine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;code&gt;*ngFor&lt;/code&gt; — ignored. Like it didn’t exist.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I even replaced it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;li &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;ngFor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"let item of ['a', 'b', 'c']"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;{{&lt;/span&gt; item &lt;span class="o"&gt;}}&lt;/span&gt;&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still nothing.&lt;/p&gt;

&lt;p&gt;Then I tried this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;div &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;ngIf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Hello ngIf&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when &lt;strong&gt;that&lt;/strong&gt; didn’t render...&lt;br&gt;&lt;br&gt;
Something snapped.&lt;/p&gt;


&lt;h2&gt;
  
  
  💡 The Realization
&lt;/h2&gt;

&lt;p&gt;After spiralling a bit, I checked the docs. Then it hit me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is a standalone component. It doesn’t inherit anything — not even Angular’s own core directives.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that’s when I realised what I had missed all along:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CommonModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common&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="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;standalone&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="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommonModule&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 missing piece&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one tiny line. That was the reason everything was failing — silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Why It Happens
&lt;/h2&gt;

&lt;p&gt;In Angular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If you're using regular components inside an &lt;code&gt;NgModule&lt;/code&gt;, importing &lt;code&gt;CommonModule&lt;/code&gt; once covers all declared components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But if you're building &lt;strong&gt;standalone components&lt;/strong&gt;, they’re completely self-contained.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;That means &lt;strong&gt;you must import&lt;/strong&gt; &lt;code&gt;CommonModule&lt;/code&gt; yourself, or you won’t get &lt;code&gt;*ngIf&lt;/code&gt;, &lt;code&gt;*ngFor&lt;/code&gt;, &lt;code&gt;| date&lt;/code&gt;, &lt;code&gt;| currency&lt;/code&gt;, etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤦 Lesson from the Drama
&lt;/h2&gt;

&lt;p&gt;I was so focused on building the UI and making things work that I forgot to stop and ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Wait, where do structural directives even come from?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was a great reminder:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Even as someone experienced, it's easy to fall into habits — and overlook small things that matter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The truth is, I wasn't "doing Angular" — I was doing muscle memory.&lt;/p&gt;

&lt;p&gt;And Angular? It has changed.&lt;/p&gt;


&lt;h2&gt;
  
  
  ✅ The Fix
&lt;/h2&gt;

&lt;p&gt;This one line saved my sanity:&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="nx"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommonModule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I added that, &lt;code&gt;*ngFor&lt;/code&gt; worked. &lt;code&gt;*ngIf&lt;/code&gt; worked. Everything came back to life.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Update: Angular 17+ Way Without CommonModule
&lt;/h2&gt;

&lt;p&gt;Starting from Angular 17, there's an even &lt;strong&gt;cleaner&lt;/strong&gt; way to handle loops without needing to import &lt;code&gt;CommonModule&lt;/code&gt; at all!&lt;/p&gt;

&lt;p&gt;✅ You can now use the new &lt;code&gt;@for&lt;/code&gt; and &lt;code&gt;@if&lt;/code&gt; syntax directly in your templates.&lt;/p&gt;

&lt;p&gt;Example:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;filterOptions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;track&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&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;option&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;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="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or conditionally:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filterOptions&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;We&lt;/span&gt; &lt;span class="nx"&gt;have&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;filterOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🏆 Best Practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For Angular 16 and below:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use &lt;code&gt;CommonModule&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For Angular 17 and above:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Prefer &lt;code&gt;@for&lt;/code&gt; and &lt;code&gt;@if&lt;/code&gt; syntax.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If you need to support older versions&lt;/strong&gt;, stick to the &lt;code&gt;*ngFor&lt;/code&gt; and &lt;code&gt;*ngIf&lt;/code&gt; approach with CommonModule.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔑 Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone components mean standalone responsibilities.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Nothing is auto-included — you explicitly manage what your component needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No CommonModule?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Then &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;*ngFor&lt;/code&gt;, no &lt;code&gt;*ngIf&lt;/code&gt;, no pipes — and no warnings either.&lt;br&gt;&lt;br&gt;
Angular will silently skip them, leaving you scratching your head.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Coming back to a framework after years?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Don't assume things work like they used to.&lt;br&gt;
Even familiar tools evolve — often in small, silent ways.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tiny details break things quietly.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Always slow down and verify the basics — they are the first place things go wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bonus for Angular 17+:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The new &lt;code&gt;@for&lt;/code&gt; and &lt;code&gt;@if&lt;/code&gt; syntax means even less boilerplate — no CommonModule needed at all!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🗯️ Have You Had “Silent Failures” Too?
&lt;/h2&gt;

&lt;p&gt;Ever spent hours debugging only to realise one import was missing?&lt;br&gt;&lt;br&gt;
Or a component didn’t behave because of a silent Angular rule?&lt;/p&gt;

&lt;p&gt;Share your moment.&lt;br&gt;&lt;br&gt;
Let’s normalise the messy middle part of learning and relearning.&lt;/p&gt;

&lt;p&gt;Because sometimes, it’s not about knowing Angular.&lt;br&gt;&lt;br&gt;
It’s about knowing what to double check.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>developer</category>
      <category>learning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Amazon Q Developer CLI Helped Me Clone EC2 in One Prompt - No Console, No YAML</title>
      <dc:creator>Avinash Dalvi</dc:creator>
      <pubDate>Wed, 16 Apr 2025 03:00:43 +0000</pubDate>
      <link>https://forem.com/aws-builders/amazon-q-developer-cli-helped-me-clone-ec2-in-one-prompt-no-console-no-yaml-2ogo</link>
      <guid>https://forem.com/aws-builders/amazon-q-developer-cli-helped-me-clone-ec2-in-one-prompt-no-console-no-yaml-2ogo</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hello Devs&lt;/strong&gt;,&lt;/p&gt;

&lt;p&gt;you might have heard about the recent release of &lt;strong&gt;Amazon Q Developer CLI&lt;/strong&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%2Fcqyyaezb9uwz4w8alum1.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%2Fcqyyaezb9uwz4w8alum1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I heard about it too, but like most tools, it sat on my radar until the moment I truly needed it - a real-time use case that made me glad I gave it a shot.&lt;/p&gt;

&lt;h3&gt;
  
  
  It started with a small request... and a potential disaster.
&lt;/h3&gt;

&lt;p&gt;One of our developers was testing a machine learning model on an EC2 instance. A regular dev task, nothing fancy.&lt;/p&gt;

&lt;p&gt;But a few hours in, he messaged me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Something’s off with the setup. I don’t want to break anything — can I get a clone of this machine to test on?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair ask. But the instance he was using was one we also used for other internal dev workflows. Any changes could have unintended consequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  My default plan? Console clicks and lots of waiting.
&lt;/h3&gt;

&lt;p&gt;Cloning an EC2 isn’t rocket science — but it’s tedious:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open AWS Console&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to EC2&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find the instance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an AMI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait for the AMI to become available&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Launch a new instance from it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manually select the same instance type, security groups, IAM role...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hope nothing breaks during this dance&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s not hard, but I’ve done it too many times to count — and it always eats up 15–20 minutes, minimum.&lt;/p&gt;

&lt;h3&gt;
  
  
  This time, I tried something different. Amazon Q Developer CLI.
&lt;/h3&gt;

&lt;p&gt;I had recently installed &lt;strong&gt;Amazon Q Developer CLI&lt;/strong&gt; after reading about it.&lt;br&gt;&lt;br&gt;
A command-line assistant that understands natural language prompts and turns them into real AWS commands? I had to try it.&lt;/p&gt;

&lt;p&gt;So instead of diving into the Console, I opened my terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;q chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And typed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Create a new EC2 instance from the existing one named &lt;code&gt;ml-base-server&lt;/code&gt;. Use the same security group and IAM role.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s it. One sentence. No flags. No resource IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happened next blew me away.
&lt;/h3&gt;

&lt;p&gt;Amazon Q Developer CLI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Identified the instance from its Name tag (&lt;code&gt;ml-base-server&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generated an AMI creation command&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Waited for the AMI to become available&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Created a new EC2 instance using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same instance type&lt;/li&gt;
&lt;li&gt;The same security group(s)&lt;/li&gt;
&lt;li&gt;The same IAM role&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Even prompted me to confirm each step before executing&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;No scrolling. No searching. Just… done.&lt;/p&gt;

&lt;h3&gt;
  
  
  The result? My dev got a clean copy. Our base setup stayed untouched.
&lt;/h3&gt;

&lt;p&gt;He could now run experiments safely.&lt;br&gt;&lt;br&gt;
No stress, no last-minute surprises, no Console hopping.&lt;/p&gt;

&lt;p&gt;What would have taken 15+ minutes manually was done in under 3 — right from my terminal.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;We often think of DevOps automation in terms of scripting or building pipelines.&lt;br&gt;&lt;br&gt;
But sometimes, what you really want is a &lt;strong&gt;conversation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s what Amazon Q Developer CLI gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You say what you want in plain English&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It figures out how to do it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You still keep control with confirmations&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small-but-important infra tasks like this, it’s a &lt;strong&gt;game-changer&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;p&gt;If you haven’t used Amazon Q Developer CLI yet, get started here:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://dev.to/aws/getting-started-with-amazon-q-developer-cli-4dkd"&gt;Getting Started with Amazon Q Developer CLI – dev.to&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once installed, try this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;q chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;“Create a new EC2 instance from the one named &lt;code&gt;your-instance-name&lt;/code&gt;. Keep the same security group and IAM role.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ⚠️ Heads Up: A Few Post-Install Steps Are Missing from the Docs (macOS)
&lt;/h2&gt;

&lt;p&gt;If you’re following the &lt;a href="https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html" rel="noopener noreferrer"&gt;official Amazon Q CLI installation guide&lt;/a&gt;, you’ll notice it stops right after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;amazon-q
q &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if you're on &lt;strong&gt;macOS&lt;/strong&gt;, there are a few &lt;em&gt;extra steps&lt;/em&gt; you &lt;strong&gt;must&lt;/strong&gt; follow before things actually start working:&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Steps You Need to Complete After Installing via Brew
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open the Amazon Q desktop app&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
After the &lt;code&gt;brew install&lt;/code&gt;, you’ll find a new app called &lt;strong&gt;Amazon Q&lt;/strong&gt; installed on your system (via Launchpad or Spotlight).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grant Accessibility Permissions&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
When you launch the app for the first time, it will prompt you to &lt;strong&gt;enable accessibility access&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Go to:  System Settings → Privacy &amp;amp; Security → Accessibility&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;    Terminal.app&lt;/li&gt;
&lt;li&gt;    iTerm2&lt;/li&gt;
&lt;li&gt;    Hyper&lt;/li&gt;
&lt;li&gt;    VS Code Terminal&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%2Fffjpeac4idiqzvw52z7k.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%2Fffjpeac4idiqzvw52z7k.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then allow &lt;strong&gt;Amazon Q (CodeWhisperer)&lt;/strong&gt; to control your system.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Login to AWS from the app&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You’ll be prompted to authenticate with your AWS account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable Terminal Integration&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Go to the &lt;strong&gt;Integrations&lt;/strong&gt; tab in the sidebar, and click &lt;strong&gt;“Enable”&lt;/strong&gt; for your preferred terminal:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify it’s active&lt;/strong&gt;
Open your terminal and try typing &lt;code&gt;q chat&lt;/code&gt; — you should now see the CLI assistant activate in context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/CKye8xI0HiU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Feel free to share what you are targeting to move from manual or script work to Amazon Q Developer CLI**.** I’ve built EC2 clones hundreds of times the old way.&lt;br&gt;&lt;br&gt;
But this experience — using natural language to handle it — felt like the future.&lt;/p&gt;

&lt;p&gt;Amazon Q Developer CLI didn’t just save time.&lt;br&gt;&lt;br&gt;
It let me focus on solving the actual problem, not navigating a UI maze.&lt;/p&gt;

&lt;p&gt;And honestly?&lt;br&gt;&lt;br&gt;
I can’t wait to see what other repetitive tasks I can retire next.&lt;/p&gt;

&lt;h3&gt;
  
  
  References :
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/aws/getting-started-with-amazon-q-developer-cli-4dkd"&gt;https://dev.to/aws/getting-started-with-amazon-q-developer-cli-4dkd&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/aws/amazon-q-developer-cli" rel="noopener noreferrer"&gt;https://github.com/aws/amazon-q-developer-cli&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/posts/ricardosueiras_amazonqdevelopercli-aws-demoscene-activity-7312502995567431680-OocE/?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAAycFYBiLynyePvMJ45ZRUDGVPqRgz4AJg" rel="noopener noreferrer"&gt;https://www.linkedin.com/posts/ricardosueiras_amazonqdevelopercli-aws-demoscene-activity-7312502995567431680-OocE/?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAAycFYBiLynyePvMJ45ZRUDGVPqRgz4AJg&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>q</category>
      <category>developer</category>
    </item>
  </channel>
</rss>
