<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rick Wise</title>
    <description>The latest articles on DEV Community by Rick Wise (@cloudwiseteam).</description>
    <link>https://dev.to/cloudwiseteam</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3582447%2Fe7a88946-c7a3-4aad-9242-6d52380c09f1.png</url>
      <title>DEV Community: Rick Wise</title>
      <link>https://dev.to/cloudwiseteam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudwiseteam"/>
    <language>en</language>
    <item>
      <title>I Shipped a Redesign That Orphaned My Own Product</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:53:46 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/i-shipped-a-redesign-that-orphaned-my-own-product-5g74</link>
      <guid>https://dev.to/cloudwiseteam/i-shipped-a-redesign-that-orphaned-my-own-product-5g74</guid>
      <description>&lt;p&gt;This month I rebuilt CloudWise around an AI agent.&lt;/p&gt;

&lt;p&gt;Not a chatbot bolted into a corner. The agent &lt;strong&gt;is&lt;/strong&gt; the product now. You open the app and you're talking to it. "Where's my money going?" — and it pulls your AWS spend, ranks the waste, shows you the dollars per month per finding, and tells you what's safe to fix.&lt;/p&gt;

&lt;p&gt;It looked done. The screens were beautiful. The agent answered. I shipped it as the front door.&lt;/p&gt;

&lt;p&gt;Then I used it like a customer would. And I discovered I had quietly orphaned half my own product.&lt;/p&gt;

&lt;p&gt;This is the story of that month, because it taught me the single most useful lesson I've learned as a solo founder: &lt;strong&gt;a redesign isn't done when it looks done.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;For most of this year, CloudWise looked like every other AWS cost tool: a dashboard. Charts, tables, a sidebar of pages. Useful, but passive. You had to know what to look for.&lt;/p&gt;

&lt;p&gt;The bet I made for the overhaul was that the &lt;em&gt;interface&lt;/em&gt; should be a conversation, not a dashboard. Most people don't want to read a cost dashboard. They want to ask a question and get an answer.&lt;/p&gt;

&lt;p&gt;So the centerpiece became a conversational workspace backed by a real tool-calling agent loop on Claude (running on AWS Bedrock). "Real" is the important word. This isn't a model that summarizes a page of text. It's a model with tools — it actually queries your cost data, your findings, your Reserved Instance and Savings Plan coverage, and composes an answer from live numbers. It remembers your account between conversations.&lt;/p&gt;

&lt;p&gt;You can ask it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Where's my money going?" → it breaks down spend by service, in gold, with the month-over-month delta.&lt;/li&gt;
&lt;li&gt;"What's safe to fix?" → it ranks your waste findings: idle NAT Gateways, forgotten SageMaker notebooks, unattached EBS snapshots, each with a dollar figure and a safe-to-fix flag.&lt;/li&gt;
&lt;li&gt;"Compare my two accounts." → and it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters more than it sounds, and it's where the trouble started.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part where it looked finished
&lt;/h2&gt;

&lt;p&gt;By the start of June, the redesign was, on paper, complete. New design system. Dark, native. A conversational workspace. A guided tour. A redesigned dashboard. The agent loop. Cross-session memory. I'd migrated every screen.&lt;/p&gt;

&lt;p&gt;I flipped the new workspace to be the post-login front door and moved on.&lt;/p&gt;

&lt;p&gt;Here's the uncomfortable truth about building alone: &lt;strong&gt;you stop seeing your own product.&lt;/strong&gt; You navigate it the way the author navigates it — from the inside, knowing every shortcut, never actually starting cold the way a real user does.&lt;/p&gt;

&lt;p&gt;So I made myself start cold. I logged in like a brand-new customer and tried to do the boring things. Switch to my other AWS account. Open settings. Change a notification. Log out.&lt;/p&gt;

&lt;p&gt;I couldn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The front door had hidden the house
&lt;/h2&gt;

&lt;p&gt;None of those features were gone. Every settings page still existed. The account switcher still existed. Logout still existed. The old navigation still existed, sitting in the codebase, fully functional.&lt;/p&gt;

&lt;p&gt;They just weren't &lt;em&gt;reachable&lt;/em&gt; from the place users now landed.&lt;/p&gt;

&lt;p&gt;The new workspace shell had a clean little user footer — a static label with the account name. No menu. No logout. No link to subscription, notifications, alerts, API keys, AWS accounts, or password. From the new front door, there was literally no path to any of it.&lt;/p&gt;

&lt;p&gt;Worse, the half of the product that &lt;em&gt;did&lt;/em&gt; still have navigation rendered in the &lt;strong&gt;old&lt;/strong&gt; chrome. So a new user would land in a slick dark conversational workspace, click one thing, and get bounced into the previous design — a completely different layout. Two products wearing different clothes, stitched together at a seam the user falls straight through.&lt;/p&gt;

&lt;p&gt;I wrote it down plainly in my own audit doc at the time, because I needed to see it without flinching:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The redesign did not delete your features — the cutover orphaned them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence reframed the entire month. This wasn't a teardown. It was a &lt;em&gt;finishing&lt;/em&gt; problem. The work wasn't to rebuild — it was to take ownership of every job the old interface used to do, and make the new one do it better.&lt;/p&gt;




&lt;h2&gt;
  
  
  The agent was also lying about multi-account
&lt;/h2&gt;

&lt;p&gt;While I was in there, I found a real bug — the kind that only surfaces when you use the product for real, with more than one account.&lt;/p&gt;

&lt;p&gt;CloudWise is a multi-account tool. You connect your AWS Organization and it discovers all your accounts. But the agent was collapsing them. When you asked about cost, it blended every account together with no way to scope to one. And when you asked about Reserved Instance and Savings Plan coverage, it reported coverage for &lt;strong&gt;exactly one account&lt;/strong&gt; — the first one it happened to grab — and silently ignored the rest.&lt;/p&gt;

&lt;p&gt;That's not a cosmetic miss. That's the tool confidently giving you a wrong answer about money. If you had three accounts and asked "how's my RI coverage," it answered for one-third of your footprint and didn't tell you.&lt;/p&gt;

&lt;p&gt;The backend could already filter by account. The old reports page could already do multi-account selection. The &lt;em&gt;agent&lt;/em&gt; — the new centerpiece — was strictly less capable than both. The redesign had, in this one spot, made the product worse while looking like it made it better.&lt;/p&gt;

&lt;p&gt;So the fix had three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A header account switcher&lt;/strong&gt; that scopes the entire workspace — single or multi-select, capped by your plan tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-account arguments on the agent's tools&lt;/strong&gt;, so the model can honor that switcher &lt;em&gt;and&lt;/em&gt; answer in-conversation requests like "compare my two accounts."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixing the coverage bug&lt;/strong&gt; so it reports across all accounts, not the first one.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: commitments reported for whatever account came first.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_commitments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accounts&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch_coverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# silently ignores the rest
&lt;/span&gt;
&lt;span class="c1"&gt;# After: the agent scopes to what you asked for — one, some, or all.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_commitments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scope_ids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scope_ids&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;fetch_coverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small diff. A big difference in whether you can trust the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Thirty days of taking ownership
&lt;/h2&gt;

&lt;p&gt;That's what the month actually was. Not "add features." Re-own the basics, and make the agent the real front door instead of a beautiful demo sitting on top of a product it had stopped being responsible for.&lt;/p&gt;

&lt;p&gt;In order, here's what changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One app shell, not two.&lt;/strong&gt; Unified chrome around every authenticated page — one persistent sidebar, the account switcher, every settings link, and logout — so you never fall through the seam between the new workspace and the old layout again. The workspace became one surface &lt;em&gt;inside&lt;/em&gt; a consistent app, not a separate world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The account switcher scopes everything.&lt;/strong&gt; Pick an account (or several) in the header and the whole workspace re-scopes to it — and the agent answers for exactly that selection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent learned to count accounts.&lt;/strong&gt; Per-account tool arguments, the coverage bug fixed, and the ability to genuinely compare accounts on request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A deep, cross-page guided tour.&lt;/strong&gt; Instead of a checklist nobody reads, a docked "CloudWise agent" companion walks a new user through the real product — navigating the actual screens across seventeen steps, not narrating a slideshow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The whole product went dark-mode-native.&lt;/strong&gt; Every deep page — cost reports, remediation, savings plans, settings, the dashboard — rebuilt on one design system. Money is always gold. No more half-migrated screens where one page is dark and the next is white.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shareable cost-health score.&lt;/strong&gt; One number, 0–100, for how efficiently you're running AWS — with a public, sanitized share card carrying no account details or PII. Because the first question every engineer actually asks is "are we good, or not?"&lt;/p&gt;

&lt;p&gt;Across the month that came out to a few dozen shipped changes and around fifty releases — versions 1.54 through 1.104. Most of them were not glamorous. Most of them were re-owning a job the old interface used to do, quietly, that the new one had dropped on the floor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lesson, stated plainly
&lt;/h2&gt;

&lt;p&gt;I've shipped a lot of software in twenty-five years. I still fell for this one, which is why I think it's worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Looks done" and "is the product" are not the same state.&lt;/strong&gt; They can be a hundred releases apart.&lt;/p&gt;

&lt;p&gt;A redesign is not finished when the new screens are beautiful and the happy path works. It's finished when the new thing has quietly taken over &lt;em&gt;every&lt;/em&gt; job the old thing did — logout, account switching, the boring settings page nobody screenshots — and does each of them at least as well. Until then you don't have a new product. You have a beautiful front door on a house whose rooms you've locked.&lt;/p&gt;

&lt;p&gt;And the cruelest part: &lt;strong&gt;users don't grade you on the demo.&lt;/strong&gt; They grade you on the one ordinary day they need the single feature you forgot to bring across. The day they need to switch accounts, or check their RI coverage across all three, or just sign out. That's the moment your redesign is actually judged — and it's never the moment you rehearsed.&lt;/p&gt;

&lt;p&gt;So now I have a rule. Before I call any cutover done, I log in cold and do the boring things. All of them. If I can't sign out, I'm not done. No matter how good the front door looks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CloudWise is an AWS cost optimization tool for startups — 191 automated waste checks, a real agent that runs against your actual usage, air-gapped mode for security teams, starting at $19/month. If you want to ask an agent where your AWS money is going, it's at &lt;a href="https://cloudcostwise.io" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>buildinpublic</category>
      <category>saas</category>
    </item>
    <item>
      <title>EBS Snapshot Sprawl: The Waste Cost Explorer Can't Show You</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:51:19 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/ebs-snapshot-sprawl-the-waste-cost-explorer-cant-show-you-5had</link>
      <guid>https://dev.to/cloudwiseteam/ebs-snapshot-sprawl-the-waste-cost-explorer-cant-show-you-5had</guid>
      <description>&lt;p&gt;Last week's short covered the 101 on old EBS snapshots: the 90-day threshold, the $0.05/GB-month rate, &lt;code&gt;aws ec2 delete-snapshot&lt;/code&gt;. If you saw it, you already know snapshots are cheap-per-unit and expensive-in-aggregate. What that 42 seconds couldn't fit in is the actual reason snapshot sprawl is so hard to find in the first place — and it isn't the price. It's that Cost Explorer structurally cannot show you who's responsible for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Explorer shows you a bill, not a culprit
&lt;/h2&gt;

&lt;p&gt;Group your AWS costs by usage type and EBS snapshots show up as one line: &lt;code&gt;EBS:SnapshotUsage&lt;/code&gt;, rolled up per account and region. That's it. Not per-snapshot, not per-volume, not per-AMI. If that line is $340/month, Cost Explorer will tell you the total and nothing about which of your 200 snapshots — or which of your 40 AMIs pinning them — put it there.&lt;/p&gt;

&lt;p&gt;Part of why this is so opaque is how the billing actually works. EBS snapshots are &lt;strong&gt;incremental&lt;/strong&gt;: the first snapshot of a volume captures every block, but every snapshot after that only stores blocks that changed since the previous one. Delete an "old" snapshot in the middle of a chain and AWS doesn't just drop its unique blocks — it can merge the still-referenced blocks from that snapshot into the next one to keep the chain valid. The result is a genuinely well-designed storage model that happens to make per-snapshot cost attribution close to meaningless from the outside. You can't look at snapshot #14 in a chain of 20 and know what deleting it actually frees, and Cost Explorer doesn't even try — it just gives you the account-wide sum and moves on.&lt;/p&gt;

&lt;p&gt;So the number you see going up every month is real. The reason is invisible from the billing console. You have to go look at the resources directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;deregistered ≠ deleted&lt;/code&gt; — the AMI-pinning trap
&lt;/h2&gt;

&lt;p&gt;Here's the gap that costs teams the most and gets found the least: deregistering an AMI does not delete the snapshot backing it.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;CreateImage&lt;/code&gt; against an EC2 instance, AWS creates an AMI and, silently, one or more EBS snapshots to back it — you'll find the linkage in the snapshot's own description field, something like &lt;code&gt;Created by CreateImage(i-0abc123def456789) for ami-0fedcba987654321&lt;/code&gt;. Deregister that AMI later (cleaning up an old release, retiring a pipeline, whatever the reason) and AWS removes the AMI. The snapshot stays. Forever. Nothing in the console flags it, nothing in Cost Explorer changes shape, and nothing tells you the thing that snapshot was created for no longer exists.&lt;/p&gt;

&lt;p&gt;This is exactly the pattern our &lt;code&gt;AMI_ORPHANED_SNAPSHOT&lt;/code&gt; detector checks for. It's not a guess — it's a direct read of the relationship AWS itself records: the detector regex-matches each snapshot's description against the &lt;code&gt;CreateImage(...)  for (ami-...)&lt;/code&gt; pattern, pulls the referenced AMI ID, and checks it against the account's currently-registered AMIs. If the AMI isn't there anymore, the snapshot is flagged — at higher priority than a generic "old snapshot" check, because an orphaned-AMI snapshot has a &lt;strong&gt;certain&lt;/strong&gt; reason to be dead, not just an age-based guess.&lt;/p&gt;

&lt;p&gt;That priority ordering matters. Our storage detector dedups three overlapping checks against the same snapshot inventory: AMI-orphan first, then volume-orphan (the source volume was deleted), then plain age (over the 90-day default threshold, at the $0.05/GB-month rate from last week's short). A snapshot only gets counted once, under whichever explanation is strongest — an AMI-orphaned snapshot isn't also reported as merely "old," because "old" undersells why it's actually safe to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle policies are the actual fix — for snapshots you haven't made yet
&lt;/h2&gt;

&lt;p&gt;None of the above is a criticism of EBS snapshots as a backup mechanism. They're cheap, they're incremental, and they're the right tool. The problem is entirely operational: nothing deletes them automatically unless you tell it to.&lt;/p&gt;

&lt;p&gt;AWS Data Lifecycle Manager (DLM) exists for exactly this — attach a policy to a tag or resource type and it will create snapshots on a schedule and expire them on a schedule, so "backup taken 400 days ago for a server that's been gone for 399 of them" stops being possible going forward. If you're not running DLM policies today, that's the highest-leverage 20-minute fix here, full stop.&lt;/p&gt;

&lt;p&gt;But DLM only prevents new sprawl. It does nothing for the snapshots already sitting in your account from AMIs someone deregistered two years ago, or backups nobody automated before DLM was set up. That backlog needs to be found once, by hand or by a scan, before a policy can keep it clean going forward. That's the gap our detector is built for — not a replacement for lifecycle policies, the thing that finds what predates them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding yours
&lt;/h2&gt;

&lt;p&gt;If you want to see this in your own account instead of grepping AMI descriptions by hand, &lt;a href="https://cloudcostwise.io?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=ebs-snapshot-sprawl-waste-cost-explorer-cant-show-you" rel="noopener noreferrer"&gt;a free scan&lt;/a&gt; checks this along with 190+ other waste patterns across 40+ AWS services — read-only, five minutes, nothing gets deleted without you clicking it. If you'd rather watch the 90-day/$0.05-per-GB basics first, we posted a 30-second walkthrough of that detector last week.&lt;/p&gt;

&lt;p&gt;Or check the AMI-pinning trap yourself right now:&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;# Snapshots whose description names a CreateImage/AMI pairing&lt;/span&gt;
aws ec2 describe-snapshots &lt;span class="nt"&gt;--owner-ids&lt;/span&gt; self &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Snapshots[?contains(Description, 'CreateImage')].[SnapshotId,Description]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Currently-registered AMI IDs&lt;/span&gt;
aws ec2 describe-images &lt;span class="nt"&gt;--owners&lt;/span&gt; self &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Images[].ImageId"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cross-reference the AMI ID in each snapshot's description against the second list. Anything missing has been quietly billing you since the day someone cleaned up an AMI and assumed the cleanup was done.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>The Bug That Turned Every Bad Password Into a Server Outage</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:50:30 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/the-bug-that-turned-every-bad-password-into-a-server-outage-4dda</link>
      <guid>https://dev.to/cloudwiseteam/the-bug-that-turned-every-bad-password-into-a-server-outage-4dda</guid>
      <description>&lt;p&gt;Type your password wrong on CloudWise's login page, and for a while, the server told you it had a nervous breakdown.&lt;/p&gt;

&lt;p&gt;Not "invalid credentials." Not even a plain 401. An HTTP &lt;strong&gt;500&lt;/strong&gt; — the code reserved for "something on our end is broken" — for the most routine failure mode there is: a human mistyping a password.&lt;/p&gt;

&lt;p&gt;This happened on staging. It happened on production. It happened to &lt;code&gt;POST /api/v1/auth/login&lt;/code&gt; with wrong creds, an unknown email, an unconfirmed account, or an account mid-password-reset. Four different "you did something ordinary" situations, all reported back as "we did something wrong." It went undetected until an automated gate caught it and refused to let a release ship. This is that bug, the fix, and why the distinction matters more than it sounds like it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;_handle_cognito_auth_error&lt;/code&gt;, the function that translates AWS Cognito's auth exceptions into an HTTP response, raised a &lt;code&gt;CloudWiseException&lt;/code&gt; without ever setting a &lt;code&gt;status_code&lt;/code&gt;. No status code means the default. The default is 500.&lt;/p&gt;

&lt;p&gt;So every one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong password&lt;/li&gt;
&lt;li&gt;Email that isn't registered&lt;/li&gt;
&lt;li&gt;Account that hasn't confirmed its email yet&lt;/li&gt;
&lt;li&gt;Account stuck in a forced password reset&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;came back as a server error. Reproduced live on both environments — for example, &lt;code&gt;POST /api/v1/auth/login&lt;/code&gt; with bad credentials on staging returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP 500
{"detail":"Invalid email or password"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and on production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP 500
{"detail":"Authentication failed: User does not exist."}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that second one again. Production wasn't just returning the wrong status code — it was telling an anonymous caller whether a given email address had an account. That's a second, smaller bug riding along inside the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the status code is the actual bug
&lt;/h2&gt;

&lt;p&gt;It's tempting to shrug this off — the message was right there in the body, &lt;code&gt;"Invalid email or password"&lt;/code&gt;, so what's the harm in the wrong three-digit prefix?&lt;/p&gt;

&lt;p&gt;The harm is that the status code isn't decoration. It's the part of the response that infrastructure reads without understanding a word of the payload. A monitoring dashboard doesn't parse &lt;code&gt;detail&lt;/code&gt;. It counts 5xx rates. To CloudWise's own alerting, every single mistyped password looked identical to an actual outage — same bucket, same page-worthy signal, same "something is on fire" pattern, forever, at whatever rate normal users normally fat-finger their passwords. That's not a rare event. It's baseline noise, and the bug was quietly dressing it up as baseline crisis.&lt;/p&gt;

&lt;p&gt;It also meant every one of those routine rejections logged at &lt;code&gt;error&lt;/code&gt; level — the log level meant for things that need a human to look at them, now firing constantly for people who just needed to try again.&lt;/p&gt;

&lt;p&gt;And then it did something worse than annoy a dashboard: it blocked a production release.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually got caught
&lt;/h2&gt;

&lt;p&gt;CloudWise's release pipeline runs an E2E gate against staging before anything is allowed to promote to production. One of the negative-path assertions in that gate — &lt;code&gt;ci-gate-read.spec.ts:93&lt;/code&gt; — logs in with a wrong password and checks that the app shows an error and stays on &lt;code&gt;/auth/login&lt;/code&gt;, the way a real login form should behave.&lt;/p&gt;

&lt;p&gt;That test didn't expect a 500. Nothing in a normal auth flow should return one for a wrong password. The gate went red, and release &lt;code&gt;1.105.0&lt;/code&gt; sat there, unable to auto-promote to production, because the pipeline correctly refused to trust a build where the login form's error handling was behaving like a crash.&lt;/p&gt;

&lt;p&gt;That's the part worth sitting with: this bug was already live in production, unnoticed, for who knows how long. It took a &lt;em&gt;different&lt;/em&gt; release's gate run to surface it — not because the gate was looking for this specific bug, but because it was asserting the right general behavior (bad password → clean error, stay put) and the actual behavior didn't match. A negative-path test doesn't need to know about your bug in advance. It just needs to check that the ordinary failure case looks ordinary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The fix, in &lt;code&gt;backend/app/services/cognito_auth_service.py&lt;/code&gt;, maps Cognito's exceptions to the status codes they should have had all along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NotAuthorizedException&lt;/code&gt; (wrong password) and &lt;code&gt;UserNotFoundException&lt;/code&gt; (unknown email) → &lt;strong&gt;401&lt;/strong&gt;, both returning the exact same message: &lt;code&gt;"Invalid email or password"&lt;/code&gt;. Same message for both cases on purpose — the response can no longer be used to tell whether an email is registered, closing that account-enumeration leak.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UserNotConfirmedException&lt;/code&gt; and &lt;code&gt;PasswordResetRequiredException&lt;/code&gt; → &lt;strong&gt;403&lt;/strong&gt; — the account exists, but the request is correctly rejected for a reason that isn't "try a different password."&lt;/li&gt;
&lt;li&gt;Anything else — a genuinely unexpected Cognito error — still defaults to 500. That default is correct in that case. An unrecognized failure mode &lt;em&gt;is&lt;/em&gt; a server-side concern worth alerting on. The bug was never that 500 existed; it was that the four most common, most expected auth rejections were routed into it by omission.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected-rejection logging dropped from &lt;code&gt;error&lt;/code&gt; to &lt;code&gt;warning&lt;/code&gt;, so the logs now reflect what actually happened: a routine, anticipated rejection, not an incident.&lt;/p&gt;

&lt;p&gt;The test suite got the fix that should have caught this the first time. &lt;code&gt;test_handle_cognito_auth_error_*&lt;/code&gt; previously asserted on the error &lt;em&gt;message&lt;/em&gt; only — never the status code. That's exactly how a 500-instead-of-401 slips through code review and CI both: the message text looked fine, so nobody noticed the number in front of it was wrong. The tests now assert status codes explicitly, plus two cases that weren't covered before: the no-leak &lt;code&gt;UserNotFoundException&lt;/code&gt; path, and &lt;code&gt;PasswordResetRequiredException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the sibling of an earlier fix, CLO-48, which cleaned up the same bug class on the refresh-token path — an expired refresh token was logging as &lt;code&gt;ERROR&lt;/code&gt; twice when a clean 401 was all that was warranted. That one didn't touch login. This one closes the login path CLO-48 didn't cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Status codes are a contract between your API and everything that reads it without understanding it — monitors, alerting rules, retry logic, browsers, other services. A wrong password is a client error. Treating it as a server error doesn't just mislabel one response; it teaches every downstream system that watches your 5xx rate to distrust the signal, right when you need that signal to mean something.&lt;/p&gt;

&lt;p&gt;The thing that actually caught this wasn't a code review, a manual QA pass, or a customer complaint. It was an automated negative-path assertion doing exactly what negative-path assertions are for: checking that the boring, expected failure looks boring and expected. It's staying in the gate. Nobody has to remember to test this again — the pipeline already refuses to ship a build that gets it wrong.&lt;/p&gt;

&lt;p&gt;If you want to see what else that kind of scrutiny turns up, &lt;a href="https://cloudcostwise.io?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=login-returned-500-bug-story" rel="noopener noreferrer"&gt;a free, read-only scan&lt;/a&gt; of your AWS account takes about five minutes and changes nothing without you approving it first.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>testing</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Inside a Real $11,871/mo AWS Waste Scan</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:50:30 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/inside-a-real-11871mo-aws-waste-scan-5536</link>
      <guid>https://dev.to/cloudwiseteam/inside-a-real-11871mo-aws-waste-scan-5536</guid>
      <description>&lt;p&gt;Most cost-optimization content shows you a screenshot of a dashboard and asks you to trust the number. So instead, here's an actual scan CloudWise runs internally as a demo profile — a mid-size SaaS company's AWS account, $28,500/mo in total spend, run through all 191 detectors. It came back with &lt;strong&gt;$11,871/mo in identified savings across 18 findings.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I want to walk through what's actually in that list, because the shape of it surprised me even after building the detectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 18 findings live
&lt;/h2&gt;

&lt;p&gt;Split by category, not dollar amount:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database — 9 findings.&lt;/strong&gt; By far the largest bucket. A mix of idle RDS instances, an oversized analytics database running at 12% CPU, stale manual snapshots, and a cluster of ElastiCache-specific findings (idle replicas, an engine migration opportunity, a serverless-fit opportunity, and one very large data-tiering opportunity — more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute — 2 findings.&lt;/strong&gt; An idle bastion host nobody SSHs into anymore, and one oversized EC2 fleet running at 15% CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage — 2 findings.&lt;/strong&gt; An unattached EBS volume left over from a migration, and a batch of aging EBS snapshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commitment Management — 2 findings.&lt;/strong&gt; A Savings Plan expiring in 52 days, and a Convertible Reserved Instance sitting on previous-generation hardware with a free exchange available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network — 2 findings.&lt;/strong&gt; Two idle Global Accelerators — one genuinely idle, one just disabled but still billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purchase Optimization — 1 finding.&lt;/strong&gt; Production RDS instances that have been running on-demand, 24/7, for 90+ days with no Reserved Instance covering them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nine of eighteen findings sitting in "Database" isn't a coincidence — it's the category where the widest range of waste patterns overlap: idle instances, oversized instances, stale snapshots, and now a whole sub-family of ElastiCache-specific checks (engine choice, replication topology, traffic shape, data tiering) that didn't exist as separate detectors a year ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one number that matters most
&lt;/h2&gt;

&lt;p&gt;Of the $11,871/mo total, &lt;strong&gt;one single finding accounts for 68% of it: $8,057/mo&lt;/strong&gt;, from an ElastiCache data-tiering opportunity.&lt;/p&gt;

&lt;p&gt;The cluster in question runs 4× &lt;code&gt;cache.r6g.16xlarge&lt;/code&gt; nodes — memory-only, no local SSD — for 1,676 GiB of total cache capacity. ElastiCache's R6gd family adds local NVMe storage and &lt;em&gt;tiers&lt;/em&gt; data between RAM and SSD automatically, based on access frequency. For a dataset where most of the data isn't accessed on every request (true of almost every real cache), that means the same effective capacity fits on far less hardware: 1× &lt;code&gt;cache.r6gd.16xlarge&lt;/code&gt; instead of 4× &lt;code&gt;cache.r6g.16xlarge&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's not a "you forgot to delete something" finding. It's an architecture-level rightsizing call that requires reading how your own cache is actually used before you touch it — which is exactly why it's flagged as &lt;code&gt;confidence: medium&lt;/code&gt;, not &lt;code&gt;high&lt;/code&gt;, and comes with an explicit risk note: SSD-resident data has slightly higher latency, so it's a good fit only when a meaningful chunk of your data is cold.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the next four biggest opportunities have in common
&lt;/h2&gt;

&lt;p&gt;Set aside the $8,057/mo ElastiCache finding and look at what else shows up near the top of this scan: an expiring Savings Plan, an unpurchased Reserved Instance opportunity on production RDS, an oversized analytics database at 12% CPU, and an oversized EC2 fleet at 15% CPU.&lt;/p&gt;

&lt;p&gt;None of those are "click delete." Every one of them is a standing decision that has to be made again, deliberately, on some recurring cadence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Savings Plan doesn't renew itself — someone has to decide, before it lapses, whether the workload underneath it still looks the same.&lt;/li&gt;
&lt;li&gt;An RDS instance rightsized today can be wrong again in six months if the workload grows, shrinks, or changes shape.&lt;/li&gt;
&lt;li&gt;"Buy the Reserved Instance" is itself a bet on the next 12 months looking like the last 3 — which is exactly the kind of call &lt;a href="https://dev.to/blog/aws-reserved-instance-commitment-risk"&gt;CloudWise's Commitment Risk Score&lt;/a&gt; exists to check before you make it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare that to the &lt;em&gt;idle&lt;/em&gt;-resource findings in this same scan — the bastion host, the unattached EBS volume, the stale snapshots, the disabled accelerators. Those are real money too, and they're the easiest to fix: find it, delete it, done. But they're structurally small, because once you delete something, it stays deleted. It doesn't come back next quarter.&lt;/p&gt;

&lt;p&gt;Rightsizing and commitment decisions aren't like that. The right instance size and the right commitment level are correct &lt;em&gt;for right now&lt;/em&gt; — and they drift the moment your traffic pattern, team, or architecture changes, which for a growing company is constantly. That's the actual thesis worth taking from a scan like this: &lt;strong&gt;waste renews itself.&lt;/strong&gt; The idle-resource sweep is a one-time cleanup. The rightsizing and commitment layer is a standing job, and it's where most of the money in this particular scan actually lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for how you should read &lt;em&gt;your&lt;/em&gt; scan
&lt;/h2&gt;

&lt;p&gt;If you run a scan and the top finding is "delete this idle volume," fix it and move on — there's nothing recurring about it. But if your biggest findings look more like this account's — an oversized instance, an expiring commitment, a data-tiering opportunity that requires understanding your own traffic — treat that as a signal that the fix isn't a single action, it's a review cadence you don't currently have. That's a different kind of problem, and it's worth naming as one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CloudWise is an AWS cost optimization tool for startups — 191 automated waste checks across 42 AWS services, read-only by design, starting at $19/month. Run a free scan at &lt;a href="https://cloudcostwise.io" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>We Cut Lambda Cold Starts 56% — Three Wrong Turns Before the Real SnapStart Fix</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:43:07 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/we-cut-lambda-cold-starts-56-three-wrong-turns-before-the-real-snapstart-fix-1bd1</link>
      <guid>https://dev.to/cloudwiseteam/we-cut-lambda-cold-starts-56-three-wrong-turns-before-the-real-snapstart-fix-1bd1</guid>
      <description>&lt;p&gt;CloudWise's &lt;code&gt;/dashboard&lt;/code&gt; took up to 2.3 seconds to load cold. That's the API Lambda's own cliff — not network, not the frontend. AWS Lambda SnapStart was already turned on. It was already restoring a frozen snapshot in about 600ms, which is the number SnapStart is supposed to deliver. And for two deploys, turning it on made no measurable difference to the thing that actually made the dashboard feel slow. This is the debugging path that got from there to a measured 56% cut — including the two attempts that didn't work, because the wrong-turn part is the part worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually slow
&lt;/h2&gt;

&lt;p&gt;We measured every layer before touching anything (full numbers in &lt;code&gt;docs/redesign/clo114-dashboard-speed-analysis.md&lt;/code&gt;). Four dashboard API calls, already firing in parallel — parallelizing them further was a non-issue, wall-clock already tracked the slowest call. Server compute, warm, was fine: ~200–380ms. SnapStart's own restore was fine: ~550–620ms.&lt;/p&gt;

&lt;p&gt;The problem was one specific number from CloudWatch &lt;code&gt;REPORT&lt;/code&gt; lines: on a cold (post-restore) invocation, the first request's own &lt;code&gt;Duration&lt;/code&gt; — not the restore, the handler work after the restore — was &lt;strong&gt;1,124ms at p50, up to 2,805ms at the tail&lt;/strong&gt;. SnapStart had already paid for the expensive part (a frozen Python interpreter + imports) and handed back a restored environment in 600ms. Something in the first real request was still doing ~1,100ms of work the snapshot didn't cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis: init phase vs. handler phase
&lt;/h2&gt;

&lt;p&gt;SnapStart snapshots whatever ran at &lt;em&gt;module import / init&lt;/em&gt;. Anything deferred to the request handler is still paid, lazily, on the first real invocation — that's the whole cliff. &lt;code&gt;get_settings()&lt;/code&gt; already runs at import in &lt;code&gt;app/main.py&lt;/code&gt;, and constructing &lt;code&gt;Settings&lt;/code&gt; performs the full AWS Parameter Store load — so config reads were already inside the snapshot, free. What wasn't: the boto3 session and DynamoDB client (built lazily in a factory, inside the request path) and FastAPI's &lt;code&gt;startup_event&lt;/code&gt; — Mangum runs &lt;code&gt;lifespan="auto"&lt;/code&gt; on the first request, not at import.&lt;/p&gt;

&lt;p&gt;The fix looked obvious: move both into the init phase, so they're captured in the snapshot. We rejected the alternative — Lambda provisioned concurrency — outright: it bills a kept-warm 2GB instance 24/7 regardless of traffic, and it's redundant with SnapStart, which already restores in ~600ms. Paying a standing bill to paper over a problem SnapStart half-solves is exactly the kind of waste we built this product to catch in other people's accounts. So: init-phase priming, &lt;code&gt;~$0&lt;/code&gt; incremental cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong turn #1: it shipped clean and did nothing
&lt;/h2&gt;

&lt;p&gt;PR #641 landed the priming code. Deploy went green, gates passed, prod promoted. Re-measured: cold &lt;code&gt;Duration&lt;/code&gt; p50 &lt;strong&gt;1,142ms&lt;/strong&gt; — statistically the same as the 1,124ms baseline. Priming had shipped and, as far as the numbers were concerned, changed nothing.&lt;/p&gt;

&lt;p&gt;Worse: we couldn't even tell &lt;em&gt;why&lt;/em&gt;. Every diagnostic line we'd added — "hooks registered," "SnapStart prime," even FastAPI's own unconditional startup log — was completely absent from CloudWatch across 90 minutes of cold starts. The working theory for a while was that SnapStart's init-phase logs simply don't reliably surface in CloudWatch, which would have made this nearly undebuggable from logs alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong turn #2 (well, half a turn): it was a log level
&lt;/h2&gt;

&lt;p&gt;It wasn't a SnapStart logging quirk. It was &lt;code&gt;logging.info()&lt;/code&gt;. The Lambda's effective log level was dropping &lt;code&gt;INFO&lt;/code&gt; — CloudWatch showed our &lt;code&gt;[WARNING]&lt;/code&gt; lines and nothing below them, meaning every one of our priming diagnostics had been silently filtered the whole time. We moved the key diagnostic and priming log lines to &lt;code&gt;WARNING&lt;/code&gt; (PR #648) and immediately got a real signal for the first time in this investigation. Lesson, underlined: when a fix "does nothing and also produces no logs," check the log level before you start reasoning about distributed systems semantics. We spent longer on the second hypothesis than the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual bug: &lt;code&gt;after_restore&lt;/code&gt; cleared, but never rebuilt
&lt;/h2&gt;

&lt;p&gt;With real diagnostics finally visible, we split the cold &lt;code&gt;REPORT&lt;/code&gt; lines into two groups: requests that hit a &lt;em&gt;fresh init&lt;/em&gt; (rare — a brand-new execution environment) versus requests that hit a &lt;em&gt;restored&lt;/em&gt; snapshot (the common case SnapStart exists for). The restored-and-then-requested group was still averaging &lt;strong&gt;~1,064ms&lt;/strong&gt; — basically the original cliff, just hiding in a bucket we hadn't isolated before.&lt;/p&gt;

&lt;p&gt;The cause was in our own restore hook. A boto3 session captured in a snapshot carries frozen credentials and, once a client exists, frozen TLS connection pools. After a real restore, the execution environment is new: credentials need refreshing, any frozen socket is dead. So &lt;code&gt;reset_for_restore()&lt;/code&gt; correctly &lt;em&gt;cleared&lt;/em&gt; the cached session and clients on &lt;code&gt;after_restore&lt;/code&gt; — that part was right, and necessary for correctness. What it didn't do was rebuild them. So the next request found an empty session, built one lazily, and paid almost exactly the cost priming was supposed to eliminate — just relocated from "first request ever" to "first request after every restore," which for a Lambda that scales to zero between sparse dashboard loads is most of them.&lt;/p&gt;

&lt;p&gt;The fix (PR #649): &lt;code&gt;reset_for_restore()&lt;/code&gt; clears &lt;em&gt;and eagerly rebuilds&lt;/em&gt; — with the freshly-restored credentials — so the next request finds the session already built, not merely uncorrupted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Post-restore first-request &lt;code&gt;Duration&lt;/code&gt;, n=7 samples per version, staging:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (no priming)&lt;/td&gt;
&lt;td&gt;1,183ms&lt;/td&gt;
&lt;td&gt;1,124ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear-only (the bug)&lt;/td&gt;
&lt;td&gt;1,064ms&lt;/td&gt;
&lt;td&gt;1,060ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eager re-prime (the fix)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;652ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;490ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;p50 1,124 → 490ms, a 56% reduction.&lt;/strong&gt; Diagnostics on every restored environment now confirm the full chain worked as intended: &lt;code&gt;hooks_registered=True&lt;/code&gt;, &lt;code&gt;session_already_built=True&lt;/code&gt;, &lt;code&gt;restore_reprimed=True&lt;/code&gt;, zero errors.&lt;/p&gt;

&lt;p&gt;It's a real win, and it's not a complete one. About 3 of 7 restores in that sample still land at ≥500ms, even with the session confirmed pre-built — most likely the first actual DynamoDB &lt;em&gt;operation&lt;/em&gt; opening its own TLS connection, since boto3 connects lazily on first call rather than at client construction. Chasing that down would mean priming a real DynamoDB round-trip inside the restore hook itself, for what's probably diminishing returns against a ~490ms floor that's already good enough. We banked the 56% rather than shipping a seventh iteration.&lt;/p&gt;

&lt;p&gt;Alongside this, a frontend session-storage stale-while-revalidate cache (shipped separately, PR #639) hides both the network round-trip and any residual cold cliff on repeat visits — the dashboard's daily-batched data makes brief staleness safe — and a keep-warm EventBridge ping keeps instances hot enough that cold restores stay rare in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;None of the three wrong turns here were exotic. "The fix looks right but does nothing" was a log level. "The clear-only version regressed" was a hook that did half its job and returned success anyway, because clearing genuinely is correct and necessary — it just isn't sufficient. The instrumentation that finally cracked it wasn't clever; it was making the diagnostic state (&lt;code&gt;hooks_registered&lt;/code&gt;, &lt;code&gt;session_already_built&lt;/code&gt;, &lt;code&gt;restore_reprimed&lt;/code&gt;) observable on the request path instead of trusting init-phase logs that, it turned out, we weren't even looking at correctly.&lt;/p&gt;

&lt;p&gt;If you're chasing a Lambda cold start and the fix you shipped isn't moving the number: check what log level is actually filtering your diagnostics before you start doubting your architecture.&lt;/p&gt;

&lt;p&gt;This is the same instinct behind the product: measure what's actually happening in your AWS account before acting on it. If you want to see what that looks like pointed at your own bill, &lt;a href="https://cloudcostwise.io?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=we-cut-lambda-cold-starts-56-percent-snapstart-lessons" rel="noopener noreferrer"&gt;a free, read-only scan&lt;/a&gt; takes about five minutes and changes nothing without you approving it first.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>lambda</category>
      <category>snapstart</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Read-Only Is the Only Safe Way to Let AI Near Your AWS Account</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 12 Aug 2026 13:54:04 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/why-read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account-jj5</link>
      <guid>https://dev.to/cloudwiseteam/why-read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account-jj5</guid>
      <description>&lt;p&gt;Every AWS cost tool eventually asks you for the same terrifying thing: an IAM role. And every vendor says the same reassuring word about it: "read-only." I want to tell you exactly what that word means when CloudWise says it, because "read-only" gets used loosely enough in this industry that the word alone shouldn't be enough to trust anyone — including us. So this post is the IAM, not the marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The role you actually grant
&lt;/h2&gt;

&lt;p&gt;When you connect an AWS account through CloudWise's one-click setup, you launch exactly one CloudFormation stack: the monitoring stack. There is no button in onboarding that launches anything else — &lt;code&gt;startConnect('monitoring')&lt;/code&gt; is the only path the connect flow calls.&lt;/p&gt;

&lt;p&gt;That stack's policy (&lt;code&gt;cloudwise-cur-setup-template.yaml&lt;/code&gt;) is a bespoke allow-list we wrote and maintain, not the AWS-managed &lt;code&gt;ReadOnlyAccess&lt;/code&gt; policy. That distinction matters more than it sounds like it should: &lt;code&gt;ReadOnlyAccess&lt;/code&gt; is enormous and vague — it grants read access to almost every AWS service, including ones CloudWise has no reason to ever look at. Our policy is scoped to what a cost scan actually needs, action by action: &lt;code&gt;ec2:DescribeInstances&lt;/code&gt;, &lt;code&gt;rds:DescribeDBInstances&lt;/code&gt;, &lt;code&gt;s3:ListAllMyBuckets&lt;/code&gt;, &lt;code&gt;ce:GetCostAndUsage&lt;/code&gt;, &lt;code&gt;compute-optimizer:GetEC2InstanceRecommendations&lt;/code&gt;, and so on — read the whole thing at &lt;a href="https://cloudcostwise.io/security/permissions?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account" rel="noopener noreferrer"&gt;cloudcostwise.io/security/permissions&lt;/a&gt;, which renders the live action count straight off the template, not a number we typed into a page and forgot to update. Every single statement in that policy is a &lt;code&gt;Get*&lt;/code&gt;, &lt;code&gt;Describe*&lt;/code&gt;, &lt;code&gt;List*&lt;/code&gt;, or &lt;code&gt;BatchGet*&lt;/code&gt; call — plus &lt;code&gt;sts:GetCallerIdentity&lt;/code&gt; and &lt;code&gt;iam:SimulatePrincipalPolicy&lt;/code&gt;, which CloudWise uses to check what a role &lt;em&gt;can&lt;/em&gt; do without ever calling it. There is no &lt;code&gt;Put&lt;/code&gt;, &lt;code&gt;Create&lt;/code&gt;, &lt;code&gt;Delete&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, &lt;code&gt;Attach&lt;/code&gt;, or &lt;code&gt;Modify&lt;/code&gt; verb anywhere in that role's policy. It cannot make a single write call against your account. Not "shouldn't" — cannot; IAM will reject the attempt at the API layer before it reaches any resource.&lt;/p&gt;

&lt;p&gt;(One nuance, for the pedants, because I'd rather you catch it than an auditor: the CUR bucket in that same template does have a bucket policy permitting &lt;code&gt;s3:PutObject&lt;/code&gt; — granted to AWS's own &lt;code&gt;billingreports.amazonaws.com&lt;/code&gt; service principal, not to CloudWise. That's how AWS itself delivers your Cost and Usage Report into a bucket you own. It's AWS writing to you, not us writing to anything.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Remediation is a different stack, a different role, and a different click
&lt;/h2&gt;

&lt;p&gt;If you want CloudWise to actually &lt;em&gt;fix&lt;/em&gt; waste — stop an idle NAT gateway, delete an orphaned snapshot, right-size an instance — that is never part of onboarding. It requires deploying a second, separate CloudFormation stack (&lt;code&gt;CloudWise-Remediation&lt;/code&gt;, using &lt;code&gt;cloudwise-remediation-role.yaml&lt;/code&gt;) that you reach only from &lt;code&gt;settings/remediation&lt;/code&gt; or &lt;code&gt;setup/permissions&lt;/code&gt;, deliberately after the point where you've already seen what a read-only scan finds. Nothing in the sign-up or connect flow grants this role. You have to go looking for it.&lt;/p&gt;

&lt;p&gt;That role's policy carries an explicit &lt;strong&gt;Deny&lt;/strong&gt; block that overrides everything else, no matter what any other statement in the policy says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iam:*&lt;/code&gt;, &lt;code&gt;organizations:*&lt;/code&gt;, &lt;code&gt;sts:*&lt;/code&gt; — CloudWise can never touch identity, org structure, or assume other roles&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;secretsmanager:GetSecretValue&lt;/code&gt; / &lt;code&gt;PutSecretValue&lt;/code&gt; / &lt;code&gt;CreateSecret&lt;/code&gt; / &lt;code&gt;UpdateSecret&lt;/code&gt; — no access to your secrets, ever&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cloudtrail:DeleteTrail&lt;/code&gt;, &lt;code&gt;cloudtrail:StopLogging&lt;/code&gt;, &lt;code&gt;config:DeleteConfigRule&lt;/code&gt;, &lt;code&gt;config:StopConfigurationRecorder&lt;/code&gt;, &lt;code&gt;guardduty:DeleteDetector&lt;/code&gt; — your audit and detection trail can't be turned off&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s3:DeleteBucket&lt;/code&gt;, &lt;code&gt;ec2:DeleteVpc&lt;/code&gt;, &lt;code&gt;ec2:DeleteSubnet&lt;/code&gt;, &lt;code&gt;ec2:DeleteSecurityGroup&lt;/code&gt;, &lt;code&gt;rds:DeleteDBInstance&lt;/code&gt; — no deleting the structural stuff that would actually hurt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ec2:AuthorizeSecurityGroupIngress/Egress&lt;/code&gt;, &lt;code&gt;ec2:RevokeSecurityGroupIngress/Egress&lt;/code&gt;, &lt;code&gt;ec2:CreateSecurityGroup&lt;/code&gt; — CloudWise cannot open, close, or create a network path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kms:CreateKey/CreateGrant/Encrypt/Decrypt/GenerateDataKey*&lt;/code&gt;, &lt;code&gt;ssm:*&lt;/code&gt; — no touching encryption or Systems Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Deny statement in IAM always wins, regardless of what any Allow statement elsewhere in the same policy — or any other policy — grants. That's not a UI promise. It's how the policy evaluates at the API layer.&lt;/p&gt;

&lt;p&gt;You'll also see a small number of &lt;code&gt;Create*&lt;/code&gt; actions in that same policy file — things like &lt;code&gt;ec2:RunInstances&lt;/code&gt; or &lt;code&gt;rds:CreateDBInstance&lt;/code&gt;. Those exist for one purpose: &lt;strong&gt;rollback&lt;/strong&gt;. Before executing an approved action, CloudWise records enough state to reverse it — restart an instance it stopped, recreate a resource it deleted, restore a secret's scheduled deletion. If a fix goes wrong, or you change your mind, there's a way back. That's a narrower and more honest thing than "can create resources," and it's also narrower than "can never create resources" — so I'm not going to round it either direction. Read the actual file if you want the specifics; that's the point of publishing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing executes without your click
&lt;/h2&gt;

&lt;p&gt;Here's the part I actually care about you believing: CloudWise proposes fixes. It does not execute them on its own initiative, ever, under any circumstance we ship today.&lt;/p&gt;

&lt;p&gt;The execution path (&lt;code&gt;lambdas/remediation_executor/handler.py&lt;/code&gt;) checks a status field before it will run a single mutating API call, and that status has to read &lt;code&gt;approved&lt;/code&gt;. The only thing that writes &lt;code&gt;approved&lt;/code&gt; is a separate approval-gateway Lambda, triggered by a verified, authenticated action from you. There is no code path today that sets an action to &lt;code&gt;approved&lt;/code&gt; automatically. If you never click approve, the action sits there, proposed, forever, and nothing happens to your account.&lt;/p&gt;

&lt;p&gt;There's also a second, quieter layer under that: every mutating call CloudWise's execution role makes is tagged with a session tag — &lt;code&gt;aws:PrincipalTag/cloudwise-action&lt;/code&gt; — that CloudWise itself sets at execution time, and the role's policy can further condition on that tag. I want to be precise about what this is and isn't: it's real defense-in-depth inside our own execution path, not a lever you hold. You don't set that tag; we do. The thing you actually control is upstream of it — the approve click that has to happen before any of this fires at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "read-only" means when we say it
&lt;/h2&gt;

&lt;p&gt;So, precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding grants exactly one role&lt;/strong&gt;, scoped to &lt;code&gt;Get&lt;/code&gt;/&lt;code&gt;Describe&lt;/code&gt;/&lt;code&gt;List&lt;/code&gt;/&lt;code&gt;BatchGet&lt;/code&gt; actions, verifiable line-by-line at &lt;a href="https://cloudcostwise.io/security/permissions?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account" rel="noopener noreferrer"&gt;cloudcostwise.io/security/permissions&lt;/a&gt;. It cannot write to your account. This is the role every CloudWise customer has, whether or not they ever touch remediation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remediation is opt-in, separate, and denies the dangerous stuff outright&lt;/strong&gt; — identity, org structure, secrets, audit trails, network rules, deletion of anything structural — regardless of what else the policy grants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing mutates without your explicit approval.&lt;/strong&gt; Propose, then execute only what you approve. Not "propose, then execute unless you object."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that requires you to trust our intentions. It requires you to trust IAM evaluation semantics, which is a much smaller ask, and one you can verify yourself against files we publish rather than a page of prose we wrote about ourselves.&lt;/p&gt;

&lt;p&gt;If you want to see what the read-only role actually finds in your account, &lt;a href="https://cloudcostwise.io?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account" rel="noopener noreferrer"&gt;a free, read-only scan&lt;/a&gt; takes about five minutes, grants nothing beyond what's described above, and changes nothing without you approving it first.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>iam</category>
      <category>ai</category>
    </item>
    <item>
      <title>NAT Gateway: The Bill Nobody Reads</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Mon, 20 Jul 2026 16:03:01 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/nat-gateway-the-bill-nobody-reads-4pc2</link>
      <guid>https://dev.to/cloudwiseteam/nat-gateway-the-bill-nobody-reads-4pc2</guid>
      <description>&lt;p&gt;Group your AWS bill by service in Cost Explorer and NAT Gateway charges don't get their own row. They're folded into &lt;strong&gt;Amazon Virtual Private Cloud&lt;/strong&gt;, sitting next to VPN connections, Transit Gateway attachments, and PrivateLink endpoints. Unless you break the view down by usage type — &lt;code&gt;NatGateway-Hours&lt;/code&gt;, &lt;code&gt;NatGateway-Bytes&lt;/code&gt; — the number you're actually paying for a gateway that might be doing nothing is invisible.&lt;/p&gt;

&lt;p&gt;We &lt;a href="https://dev.to/blog/aws-nat-gateway-costs"&gt;wrote up the mechanics of that bill back in March&lt;/a&gt; — the $0.045/hour base charge, the $0.045/GB data processing fee, the multi-AZ multiplication that turns one gateway into three. That post is still accurate and worth reading if you want the full pricing breakdown. This one is about something different: what CloudWise's detector actually checks before it tells you a NAT Gateway is dead weight, and where that check still falls short.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the detector actually looks at
&lt;/h2&gt;

&lt;p&gt;The idle-NAT-gateway check lives in &lt;code&gt;cloudwise_scan_core&lt;/code&gt;'s network detector, alongside the unattached-EIP and idle-load-balancer checks — it's one pass over a VPC's network resources, not a dedicated NAT scanner. For every NAT Gateway in &lt;code&gt;available&lt;/code&gt; state, it pulls two CloudWatch metrics over a 7-day window: &lt;code&gt;ActiveConnectionCount&lt;/code&gt; and &lt;code&gt;BytesOutToDestination&lt;/code&gt;. If both are zero for the full week, it flags the gateway as &lt;code&gt;IDLE_NAT_GATEWAY&lt;/code&gt; at &lt;code&gt;HIGH&lt;/code&gt; confidence.&lt;/p&gt;

&lt;p&gt;Two design choices worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 7 days, not 1.&lt;/strong&gt; A single quiet day doesn't mean a gateway is unused — it might serve a batch job that runs Sundays, or a staging environment nobody touches on weekends. A full week with zero connections and zero bytes is a much harder signal to explain away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the estimate is conservative.&lt;/strong&gt; The finding's &lt;code&gt;monthly_savings&lt;/code&gt; uses a flat $32.40 — the hourly base charge times a 720-hour month, not the $32.85 you'd get from AWS's actual 730-hour average. It also doesn't add anything for data processing, because by definition a gateway that's flagged idle processed zero bytes in the lookback window. The number CloudWise shows you is the floor, not an estimate padded to look impressive.&lt;/p&gt;

&lt;p&gt;The finding's &lt;code&gt;risk&lt;/code&gt; field is blunt about the tradeoff, too: deleting a NAT Gateway immediately cuts internet egress for every private-subnet resource routed through it. The action is offered — &lt;code&gt;aws ec2 delete-nat-gateway&lt;/code&gt; — but nothing executes it without a human approving first. That's the same read-only-first posture behind every detector we ship, not something special-cased for NAT Gateways.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do yet
&lt;/h2&gt;

&lt;p&gt;Here's the honest gap: the detector tells you a NAT Gateway earned its keep for zero dollars this week. It doesn't tell you what to replace it with.&lt;/p&gt;

&lt;p&gt;That decision genuinely depends on what's routing through it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the only traffic is S3 or DynamoDB, a &lt;strong&gt;Gateway VPC Endpoint&lt;/strong&gt; replaces it for free — no hourly charge, no per-GB fee, ever.&lt;/li&gt;
&lt;li&gt;If it's other AWS services (Secrets Manager, ECR, CloudWatch Logs), an &lt;strong&gt;Interface VPC Endpoint&lt;/strong&gt; runs $0.01/hour per AZ plus $0.01/GB — about 78% cheaper per gigabyte than NAT, though you're paying a small hourly fee per endpoint per AZ instead of one gateway.&lt;/li&gt;
&lt;li&gt;If it's a non-production environment with real internet egress needs but low traffic and no requirement for managed HA, a self-managed &lt;strong&gt;NAT instance&lt;/strong&gt; (something like a &lt;code&gt;t4g.nano&lt;/code&gt;) can run for a few dollars a month — you trade the 24/7 base charge for patching it yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudWise doesn't make that call for you today. It's a fair next detector to build — matching an idle gateway's actual destination traffic against what a Gateway Endpoint could cover for free — but until it exists, the decision after "this is idle" is still on you. We'd rather say that plainly than imply the tool does more than it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding yours
&lt;/h2&gt;

&lt;p&gt;If you want to see this in your own account without reading CloudWatch dashboards by hand, &lt;a href="https://cloudcostwise.io?utm_source=blog&amp;amp;utm_medium=hub&amp;amp;utm_campaign=nat-gateway-bill-nobody-reads" rel="noopener noreferrer"&gt;a free scan&lt;/a&gt; checks this along with 190+ other waste patterns across 40+ AWS services — read-only, five minutes, nothing gets deleted without you clicking it. We &lt;a href="https://youtube.com/shorts/z7bP_wMccek?feature=share" rel="noopener noreferrer"&gt;posted a 30-second walkthrough of this exact detector&lt;/a&gt; if you'd rather watch than read.&lt;/p&gt;

&lt;p&gt;Or just run the two-metric check yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudwatch get-metric-statistics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/NATGateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; BytesOutToDestination &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NatGatewayId,Value&lt;span class="o"&gt;=&lt;/span&gt;nat-0abc123def456 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-v-7d&lt;/span&gt; +%Y-%m-%dT%H:%M:%S&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%S&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--period&lt;/span&gt; 86400 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statistics&lt;/span&gt; Sum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven zeros in a row is $32.40 a month for a gateway that isn't gating anything.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Why I Built a Risk Score Instead of a Buy Button</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 08 Jul 2026 17:26:13 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/why-i-built-a-risk-score-instead-of-a-buy-button-54eg</link>
      <guid>https://dev.to/cloudwiseteam/why-i-built-a-risk-score-instead-of-a-buy-button-54eg</guid>
      <description>&lt;p&gt;The obvious feature to build here is a button. "You're spending $4,200/month on &lt;code&gt;m5.xlarge&lt;/code&gt; — buy a 1-year Savings Plan and save 30%." One click, instant discount, everybody's happy.&lt;/p&gt;

&lt;p&gt;I almost built that button. I'm glad I didn't.&lt;/p&gt;

&lt;p&gt;Here's the problem with the button: it's only looking at the last three months. It has no idea whether the workload it's telling you to commit to will still exist in month nine. And a Reserved Instance or Savings Plan isn't a coupon — it's a bet, paid up front or amortized monthly, that a specific slice of your infrastructure will look roughly the same for a year or three. Get that bet wrong and the "savings" tool just talked you into a liability.&lt;/p&gt;

&lt;p&gt;So instead of a buy button, I built a &lt;strong&gt;Commitment Risk Score&lt;/strong&gt; — a feature whose entire job is to sometimes tell you &lt;em&gt;not&lt;/em&gt; to buy anything yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature that argues with itself
&lt;/h2&gt;

&lt;p&gt;The Commitment Risk Score pulls four signals straight from Cost Explorer for an account: how much your top instance families churn month to month, how volatile your spend is, how long your individual resources actually live, and how well you're already using the commitments you have. It weights them (35/25/25/15) into a single 0–100 score, and that score maps to a recommendation — anywhere from "3-year Convertible RI, take the discount" down to "on-demand and Spot only, buy nothing."&lt;/p&gt;

&lt;p&gt;That last outcome is the part that made this feature interesting to build. Most cost-optimization tools are graded on how much they tell you to save. This one is graded on how honest it is about when a "savings" purchase would actually be a mistake. It costs about $0.04/account/month to compute — four &lt;code&gt;GetCostAndUsage&lt;/code&gt; calls, refreshed weekly — the recommendation quality has nothing to do with the compute cost, so there was no excuse to cut a signal to save pennies.&lt;/p&gt;

&lt;p&gt;I wrote up the full math — the Jaccard-distance churn calculation, the coefficient-of-variation volatility threshold, the worked examples — in a separate deep dive, because the mechanics deserved their own space: &lt;a href="https://dev.to/blog/aws-reserved-instance-commitment-risk"&gt;The Commitment-Risk Score: should you buy that RI?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What almost went wrong
&lt;/h2&gt;

&lt;p&gt;The instinct that almost got me was averaging. My first pass weighted all four signals close to evenly, because "why not, they're all relevant." It took building the worked examples to see the problem: a team mid-Graviton-migration with high churn but a stable dollar total would score &lt;em&gt;fine&lt;/em&gt; on an even-weighted average, because volatility and churn partially cancel out in the wrong direction. The churn signal needed to dominate — 35%, not 25% — because a changing instance mix is structurally the most common way a commitment gets stranded, regardless of what the topline spend number is doing.&lt;/p&gt;

&lt;p&gt;The existing-waste signal ended up smallest at 15%, which felt backwards at first — isn't "you're already wasting money" the most damning fact? It is, but it's also the least &lt;em&gt;predictive&lt;/em&gt; one. It tells you about the past, not whether the next commitment will strand. It stayed in the score because it's a legitimate red flag, but it doesn't get to drown out the forward-looking signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the right shape for a cost tool
&lt;/h2&gt;

&lt;p&gt;CloudWise's whole premise is read-only: we look at your AWS usage and billing data, we never touch your infrastructure, and every dollar figure we show you needs to survive you checking it against Cost Explorer yourself. A recommendation engine that only ever says "buy more" doesn't survive that scrutiny for long — eventually it recommends a commitment that strands, and you stop trusting the number.&lt;/p&gt;

&lt;p&gt;A feature that's willing to say "not yet, here's why" is the one worth trusting the next time it says "yes, buy it." That was true before I shipped this, and building it just made me believe it more.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CloudWise is an AWS cost optimization tool for startups — 191 automated waste checks including commitment-risk scoring, read-only by design, starting at $19/month. Run a free scan at &lt;a href="https://cloudcostwise.io" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>EC2 Cost Optimization: Are You Ready to Commit to Reserved Instances?</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Wed, 10 Jun 2026 12:38:36 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/ec2-cost-optimization-are-you-ready-to-commit-to-reserved-instances-idk</link>
      <guid>https://dev.to/cloudwiseteam/ec2-cost-optimization-are-you-ready-to-commit-to-reserved-instances-idk</guid>
      <description>&lt;p&gt;RI and Savings Plan utilization rates below 80% are far more common than teams admit. The dashboards show green, the discount applied, the finance team is happy — and meanwhile a chunk of every commitment is being paid for and not used.&lt;/p&gt;

&lt;p&gt;Here's how it usually happens. You buy a 1-year EC2 Instance Savings Plan on &lt;code&gt;m5.xlarge&lt;/code&gt;. All-upfront, that's roughly $1,075 for the year. Six months in, someone runs a Graviton benchmark, the numbers are great, and you migrate the fleet to &lt;code&gt;m7g&lt;/code&gt;. That EC2 Instance Savings Plan was scoped to the &lt;code&gt;m5&lt;/code&gt; family. It doesn't follow you. You now pay for the &lt;code&gt;m7g&lt;/code&gt; capacity on-demand &lt;em&gt;and&lt;/em&gt; keep paying off the stranded &lt;code&gt;m5&lt;/code&gt; commitment. The "savings" turned into a double bill.&lt;/p&gt;

&lt;p&gt;The mistake isn't buying Reserved Instances. RIs and Savings Plans are the single biggest lever on an EC2 bill — 30% to 70% off on-demand. The mistake is buying them before your architecture is stable enough to actually use them for the full term.&lt;/p&gt;

&lt;p&gt;And the heuristic most teams use to decide is backwards. "We've been running this instance type for three months, let's commit." That's looking in the rear-view mirror. A commitment is a bet on the &lt;em&gt;next&lt;/em&gt; 12 to 36 months, not the last three. The real question is: how likely is this workload to still look like this a year from now?&lt;/p&gt;

&lt;p&gt;You can answer that with data you already have. There are four signals, each measurable from Cost Explorer or the AWS CLI, that together tell you whether you're ready to commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four signals that tell you if you're ready
&lt;/h2&gt;

&lt;p&gt;These are the four signals that combine into a single 0–100 risk score. The weights below aren't arbitrary — they reflect how much each one actually predicts a stranded commitment. You can compute every one of them yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 1 — Instance Family Churn (weight: 35%)
&lt;/h3&gt;

&lt;p&gt;This is the heaviest signal, because a changing instance mix is the most common way commitments get stranded. It measures how much your top instance families shift month over month.&lt;/p&gt;

&lt;p&gt;The method: pull Cost Explorer grouped by &lt;code&gt;INSTANCE_TYPE_FAMILY&lt;/code&gt; at monthly granularity over the last 6 months. You can do exactly that from the CLI — set the dates to your own 6-month window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ce get-cost-and-usage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-period&lt;/span&gt; &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-01-01,End&lt;span class="o"&gt;=&lt;/span&gt;2026-06-01 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--granularity&lt;/span&gt; MONTHLY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metrics&lt;/span&gt; &lt;span class="s2"&gt;"UnblendedCost"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-by&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DIMENSION,Key&lt;span class="o"&gt;=&lt;/span&gt;INSTANCE_TYPE_FAMILY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"ResultsByTime[].Groups[].{Family:Keys[0],Cost:Metrics.UnblendedCost.Amount}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each month, take the top 5 families by spend. Then measure how different consecutive months are using &lt;strong&gt;Jaccard distance&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jaccard distance = 1 − |intersection| / |union|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A distance of 0 means the two months had identical top-5 families (no churn). A distance of 1 means they shared nothing (total churn).&lt;/p&gt;

&lt;p&gt;Worked example. Say your top families this period are &lt;code&gt;{m5, r5, c5, t3, r6i}&lt;/code&gt;, and a few months later, mid-Graviton-migration, they're &lt;code&gt;{m7g, r7g, c7g, t4g, m5}&lt;/code&gt;. The only family in both sets is &lt;code&gt;m5&lt;/code&gt;. So the intersection is &lt;code&gt;{m5}&lt;/code&gt; (size 1) and the union is all 9 distinct families. That gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 − 1/9 = 0.89
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.89 is &lt;em&gt;very&lt;/em&gt; high churn. Your workload is structurally different than it was, and any family-specific commitment you bought before the migration is now mostly dead weight.&lt;/p&gt;

&lt;p&gt;The score averages the Jaccard distance across each consecutive month-pair over the window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk threshold:&lt;/strong&gt; an average Jaccard distance above &lt;strong&gt;0.3&lt;/strong&gt; over 6 months means your fleet is moving too fast for long-term, family-locked commitments. Graviton migrations, containerizing onto a different family, and ML-pipeline rebuilds all push this number up fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 2 — Spend Trend Volatility (weight: 25%)
&lt;/h3&gt;

&lt;p&gt;Churn tells you &lt;em&gt;what&lt;/em&gt; you're running; volatility tells you &lt;em&gt;how much&lt;/em&gt;. A workload can stay on the same families but swing wildly in size — and a commitment sized to a peak month is wasted in a trough month.&lt;/p&gt;

&lt;p&gt;The metric is the &lt;strong&gt;coefficient of variation (CV)&lt;/strong&gt; of your monthly EC2 spend over 6 months:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CV = (standard deviation / mean) × 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked example: six months averaging $4,200/month with a standard deviation of $1,260 gives a CV of 30%. That's a third of your spend bouncing around month to month — too much to safely commit to a fixed hourly floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk threshold:&lt;/strong&gt; CV above &lt;strong&gt;25%&lt;/strong&gt; is too unpredictable to commit. (In the scoring model, CV is doubled and capped, so a 50% CV maxes out this signal at 100.)&lt;/p&gt;

&lt;p&gt;One nuance worth internalizing: if your spend is volatile but you still want &lt;em&gt;some&lt;/em&gt; commitment, &lt;strong&gt;Compute Savings Plans are far safer here than EC2 Instance Savings Plans&lt;/strong&gt;. Compute SPs apply across families, sizes, regions, and even Fargate/Lambda, so a flexible commitment sized to your baseline survives the swings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 3 — Resource Lifecycle Duration (weight: 25%)
&lt;/h3&gt;

&lt;p&gt;This one catches ephemeral fleets. If your individual instances live for days, not months, a 1-year commitment is the wrong instrument no matter how stable the &lt;em&gt;aggregate&lt;/em&gt; spend looks.&lt;/p&gt;

&lt;p&gt;The metric is the &lt;strong&gt;median&lt;/strong&gt; number of days your individual EC2 instances stay alive — median, not mean, because a few long-lived bastion hosts will drag a mean upward and hide a fleet of short-lived workers. You can derive it from daily Cost Explorer data grouped by &lt;code&gt;RESOURCE_ID&lt;/code&gt;, or sanity-check it directly against the live fleet with one CLI call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;instance-state-name,Values&lt;span class="o"&gt;=&lt;/span&gt;running &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Reservations[].Instances[].{ID:InstanceId,Type:InstanceType,Launch:LaunchTime}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sort the output by &lt;code&gt;Launch&lt;/code&gt; and eyeball the median age.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk threshold:&lt;/strong&gt; a median instance lifetime under &lt;strong&gt;30 days&lt;/strong&gt; signals an ephemeral workload where RIs are the wrong tool. In the scoring model, a median of 30 days or less scores the maximum, and 180+ days scores zero, with a linear slide between.&lt;/p&gt;

&lt;p&gt;Context: ECS on Spot, batch processing, ephemeral ML training jobs, and aggressive scale-in all produce short lifetimes. Steady-state app servers, databases, and background workers produce long ones — and those are exactly the resources that are safe to commit to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 4 — Existing Commitment Utilization (weight: 15%)
&lt;/h3&gt;

&lt;p&gt;The lowest weight, but the most damning when it's bad — because it's direct evidence you're already wasting commitments. Before buying &lt;em&gt;more&lt;/em&gt;, look at how well you're using what you have.&lt;/p&gt;

&lt;p&gt;Check Cost Explorer → &lt;strong&gt;Savings Plans → Utilization Report&lt;/strong&gt;, plus the &lt;strong&gt;RI Utilization Report&lt;/strong&gt;. The score is simply the inverse of your average utilization: 100% utilized scores 0 (no waste), 0% utilized scores 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk threshold:&lt;/strong&gt; existing utilization under &lt;strong&gt;80%&lt;/strong&gt; means fix this &lt;em&gt;before&lt;/em&gt; buying anything new.&lt;/p&gt;

&lt;p&gt;The trap here is subtle and worth stating plainly: buying more commitments does not fix low utilization — it buries the signal. New unused capacity drags the blended utilization number around and makes the underlying waste harder to see. If you're below 80%, the next purchase isn't optimization, it's compounding a mistake.&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%2Fk51r4ijfzeque9sg9wt5.webp" 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%2Fk51r4ijfzeque9sg9wt5.webp" alt="Commitment Risk Signal Reference: the four signals, their high-risk trigger conditions, and their safe thresholds" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping your score to a commitment decision
&lt;/h2&gt;

&lt;p&gt;The four signals combine — weighted 35 / 25 / 25 / 15 — into a single 0–100 risk score. Here's how that score maps to an actual purchasing decision, mirroring the labels the model emits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low risk&lt;/strong&gt; — stable architecture, churn &lt;code&gt;&amp;lt; 0.3&lt;/code&gt;, CV &lt;code&gt;&amp;lt; 15%&lt;/code&gt;, lifecycle &lt;code&gt;&amp;gt; 90 days&lt;/code&gt;, utilization &lt;code&gt;&amp;gt; 90%&lt;/code&gt;.&lt;br&gt;
→ &lt;strong&gt;3-year Convertible RIs or a 1-year Compute Savings Plan.&lt;/strong&gt; This is the maximum-savings zone. Your architecture has earned the commitment; go take the discount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium risk&lt;/strong&gt; — some volatility, a Graviton migration partway done, moderate churn.&lt;br&gt;
→ &lt;strong&gt;1-year Compute Savings Plan only.&lt;/strong&gt; Avoid EC2 Instance Savings Plans and standard RIs here — they're too specific for a fleet still in motion. A Compute SP applies to any instance family, size, and OS, which makes it the right instrument to capture savings on your stable baseline while the rest of the fleet shifts underneath it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High risk&lt;/strong&gt; — actively containerizing, churn &lt;code&gt;&amp;gt; 0.6&lt;/code&gt;, short lifecycles, existing utilization under 70%.&lt;br&gt;
→ &lt;strong&gt;On-demand plus Spot. No new commitments.&lt;/strong&gt; Lean on Spot for fault-tolerant and interruptible workloads, and reserve any Compute SP strictly for the genuinely stable baseline portion of your compute, if there is one.&lt;/p&gt;

&lt;p&gt;One clarification that trips people up: Convertible RIs &lt;em&gt;can&lt;/em&gt; be exchanged for a different family, size, or region, so they're often pitched as "flexible." But the exchange is a manual process with real restrictions, and it's nowhere near as fluid as a Compute Savings Plan that just applies automatically. When in doubt mid-transition, the Compute SP is the safer flexible bet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15-minute audit: check your account right now
&lt;/h2&gt;

&lt;p&gt;You don't need a tool to get a first read. Here's the manual version of all four signals, end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Churn.&lt;/strong&gt; Cost Explorer → Group by &lt;strong&gt;Instance Type Family&lt;/strong&gt; → &lt;strong&gt;Monthly&lt;/strong&gt; granularity → last &lt;strong&gt;6 months&lt;/strong&gt;. Compare the top-5 families month over month. If the set is visibly reorganizing — new families entering, old ones dropping out — that's churn. Eyeball whether a typical month-to-month change shares fewer than ~3 of its top 5 families with the prior month (that's roughly the 0.3 distance line).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Volatility.&lt;/strong&gt; Export your monthly EC2 totals for the same 6 months. Calculate the mean and standard deviation, then &lt;code&gt;CV = std_dev / mean × 100&lt;/code&gt;. If CV is above 25%, flag it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Lifecycle.&lt;/strong&gt; Run the &lt;code&gt;describe-instances&lt;/code&gt; command from Signal 3, sort by &lt;code&gt;LaunchTime&lt;/code&gt;, and estimate the median instance age in days. Under 30 days is a red flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Utilization.&lt;/strong&gt; Cost Explorer → &lt;strong&gt;Savings Plans → Utilization Report&lt;/strong&gt; for the last 3 months. If you're under 80%, stop — understand &lt;em&gt;why&lt;/em&gt; before you buy anything else.&lt;/p&gt;

&lt;p&gt;Fifteen minutes of Cost Explorer and one CLI command will tell you more about your readiness to commit than any "we've been running this for a while" gut check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated detection
&lt;/h2&gt;

&lt;p&gt;If you'd rather have this calculated automatically than pull Cost Explorer data by hand every quarter, CloudWise computes a &lt;strong&gt;Commitment Risk Score&lt;/strong&gt; as part of its RI/SP management view (shipped June 2, 2026). It combines the same four signals using the weights above and returns a 0–100 score, a label (LOW / MEDIUM / HIGH / CRITICAL), and a recommended maximum commitment term — so the buy/don't-buy decision comes with the math already done. You can find it at &lt;a href="https://cloudcostwise.io" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;The discount on a Reserved Instance is real, but it's only a discount if you use the whole term. Run the four signals first. If your architecture is stable, commit with confidence and take the savings. If it's moving, stay flexible — a Compute Savings Plan on your baseline, Spot for the rest — and revisit when the numbers settle. The worst outcome isn't paying on-demand a little longer. It's locking in a year of spend on a fleet you've already left behind.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>ElastiCache Pricing Breakdown: Where the Money Actually Goes</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Thu, 16 Apr 2026 14:29:01 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/elasticache-pricing-breakdown-where-the-money-actually-goes-1jc5</link>
      <guid>https://dev.to/cloudwiseteam/elasticache-pricing-breakdown-where-the-money-actually-goes-1jc5</guid>
      <description>&lt;p&gt;ElastiCache looks straightforward on the bill. You pick a node type, maybe add a replica for high availability, and move on. Then the invoice arrives and the number is bigger than the mental math suggested.&lt;/p&gt;

&lt;p&gt;The gap usually comes from one of five places: engine choice, replication topology, extended support surcharges, idle clusters, or oversized nodes nobody ever right-sized. Let's break down exactly how ElastiCache charges — and where teams get surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Engines, Three Price Points
&lt;/h2&gt;

&lt;p&gt;ElastiCache supports three engines: Valkey, Redis OSS, and Memcached. They don't cost the same.&lt;/p&gt;

&lt;p&gt;Valkey is &lt;strong&gt;20% cheaper&lt;/strong&gt; than Redis OSS and Memcached for node-based clusters, and &lt;strong&gt;33% cheaper&lt;/strong&gt; on ElastiCache Serverless. This isn't a promotional rate — it's the permanent pricing structure AWS launched with Valkey.&lt;/p&gt;

&lt;p&gt;For context, a cache.r7g.xlarge in us-east-1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Hourly Rate&lt;/th&gt;
&lt;th&gt;Monthly (730 hrs)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valkey&lt;/td&gt;
&lt;td&gt;$0.3496&lt;/td&gt;
&lt;td&gt;~$255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis OSS&lt;/td&gt;
&lt;td&gt;$0.437&lt;/td&gt;
&lt;td&gt;~$319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memcached&lt;/td&gt;
&lt;td&gt;$0.437&lt;/td&gt;
&lt;td&gt;~$319&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Prices shown for us-east-1, On-Demand.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's a $64/month difference per node on a single instance type. Multiply that across a 12-node cluster and you're looking at $768/month — just from engine choice. If you're running Redis OSS and don't need Redis-specific features that Valkey doesn't support, the migration saves real money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node-Based Pricing: You Pay Whether the Cache Is Hit or Not
&lt;/h2&gt;

&lt;p&gt;ElastiCache charges per node-hour from the moment a node is launched until it's terminated. Partial hours are billed as full hours. There is no scale-to-zero.&lt;/p&gt;

&lt;p&gt;A few common node types and what they cost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node Type&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Hourly Rate&lt;/th&gt;
&lt;th&gt;Monthly (730 hrs)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cache.t3.micro&lt;/td&gt;
&lt;td&gt;0.5 GiB&lt;/td&gt;
&lt;td&gt;$0.017&lt;/td&gt;
&lt;td&gt;~$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache.m5.large&lt;/td&gt;
&lt;td&gt;6.38 GiB&lt;/td&gt;
&lt;td&gt;$0.156&lt;/td&gt;
&lt;td&gt;~$114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache.r7g.xlarge&lt;/td&gt;
&lt;td&gt;26.32 GiB&lt;/td&gt;
&lt;td&gt;$0.437&lt;/td&gt;
&lt;td&gt;~$319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache.r6g.16xlarge&lt;/td&gt;
&lt;td&gt;419.09 GiB&lt;/td&gt;
&lt;td&gt;$5.254&lt;/td&gt;
&lt;td&gt;~$3,835&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Prices shown for Redis OSS / Memcached in us-east-1, On-Demand. Valkey is 20% lower.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The important thing to internalize: a cache.t3.micro sitting idle costs the same $12/month as one handling thousands of requests per second. The meter runs on time, not usage.&lt;/p&gt;

&lt;p&gt;AWS recommends reserving 25% of a node's memory for non-data use (replication buffers, OS overhead, etc.), so the usable capacity of a cache.r7g.xlarge is roughly 19.74 GiB, not 26.32 GiB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replication Multiplies the Bill
&lt;/h2&gt;

&lt;p&gt;Most production deployments use replication for high availability. With Redis OSS or Valkey, you configure a replication group with a primary node and one or more replica nodes per shard.&lt;/p&gt;

&lt;p&gt;Every replica is a full node charged at the same hourly rate.&lt;/p&gt;

&lt;p&gt;A three-shard cluster with one replica per shard using cache.r7g.xlarge (Valkey):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 shards × 2 nodes per shard = 6 nodes
6 × $0.3496/hr = $2.10/hr → ~$1,531/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a second replica for read scaling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 shards × 3 nodes per shard = 9 nodes
9 × $0.3496/hr = $3.15/hr → ~$2,297/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus, multi-AZ replication generates cross-AZ data transfer at $0.01/GiB in each direction. For a high-throughput cache doing 100,000 requests/second with 500-byte objects, that's roughly 167 GiB/hour of traffic. If 50% crosses AZ boundaries, that's an extra $0.84/hour — about $613/month in data transfer alone.&lt;/p&gt;

&lt;p&gt;Teams often enable multi-AZ replication on dev and staging environments where a single node would be fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serverless: Simpler, But Not Always Cheaper
&lt;/h2&gt;

&lt;p&gt;ElastiCache Serverless removes the node sizing decision entirely. You pay for two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data stored&lt;/strong&gt; — billed in GB-hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ElastiCache Processing Units (ECPUs)&lt;/strong&gt; — a unit combining vCPU time and data transferred&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Valkey&lt;/th&gt;
&lt;th&gt;Redis OSS&lt;/th&gt;
&lt;th&gt;Memcached&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data storage&lt;/td&gt;
&lt;td&gt;$0.084/GB-hr&lt;/td&gt;
&lt;td&gt;$0.125/GB-hr&lt;/td&gt;
&lt;td&gt;$0.125/GB-hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECPUs&lt;/td&gt;
&lt;td&gt;$0.0023/M&lt;/td&gt;
&lt;td&gt;$0.0034/M&lt;/td&gt;
&lt;td&gt;$0.0034/M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimum data stored&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Prices shown for us-east-1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A simple GET or SET transferring under 1 KB consumes 1 ECPU. A command transferring 3.2 KB consumes 3.2 ECPUs. Commands that use more vCPU time (like SORT or ZADD) consume proportionally more.&lt;/p&gt;

&lt;p&gt;Serverless can be cheaper for spiky workloads because you don't over-provision for peaks. But for stable, high-throughput workloads, node-based clusters are often significantly cheaper. AWS's own Example 2 shows a spiky workload costing $2.92/hour serverless vs. $5.66/hour on-demand nodes — but for steady traffic, the math can flip the other way.&lt;/p&gt;

&lt;p&gt;The minimum charge matters too. A Serverless cache for Redis OSS or Memcached is metered for at least 1 GB of data stored — roughly $91/month minimum even if you're storing almost nothing. Valkey's 100 MB minimum brings that floor down to about $6/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extended Support: The Surcharge Nobody Budgets For
&lt;/h2&gt;

&lt;p&gt;When a Redis OSS or Memcached engine version reaches end-of-life, AWS continues providing security patches through Extended Support — at a steep premium.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Surcharge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Year 1–2 after EOL&lt;/td&gt;
&lt;td&gt;80% premium on node-hour rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 3 after EOL&lt;/td&gt;
&lt;td&gt;160% premium on node-hour rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A cache.m5.large running Redis 5 (EOL January 31, 2026) at $0.156/hour becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Year 1–2:&lt;/strong&gt; $0.156 + ($0.156 × 80%) = &lt;strong&gt;$0.281/hour&lt;/strong&gt; (~$205/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Year 3:&lt;/strong&gt; $0.156 + ($0.156 × 160%) = &lt;strong&gt;$0.406/hour&lt;/strong&gt; (~$296/month)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's nearly triple the base cost by year three. Teams that don't track engine versions can drift into Extended Support without realizing their bill just jumped 80%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup Storage and Data Transfer
&lt;/h2&gt;

&lt;p&gt;Two cost categories that don't appear under the main "ElastiCache" line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup storage:&lt;/strong&gt; $0.085/GiB per month for all regions. No data transfer charges for creating or restoring backups. This is generally small unless you're snapshotting large clusters frequently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data transfer:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same AZ (EC2 ↔ ElastiCache)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-AZ (same Region)&lt;/td&gt;
&lt;td&gt;$0.01/GiB each way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Region (Global Datastore)&lt;/td&gt;
&lt;td&gt;$0.02/GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cross-AZ charge is easy to miss because it shows up as EC2 data transfer on the bill, not ElastiCache. You're only charged for the EC2 side — there's no ElastiCache data transfer charge for traffic in or out of the node itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Tiering: The Cost Saver Most Teams Don't Know About
&lt;/h2&gt;

&lt;p&gt;R6gd nodes combine memory and NVMe SSD, automatically moving least-frequently-accessed data to SSD. You get nearly 5× the total storage capacity compared to memory-only R6g nodes.&lt;/p&gt;

&lt;p&gt;AWS's example: a 1 TiB dataset needs 1 cache.r6gd.16xlarge node ($9.98/hour) vs. 4 cache.r6g.16xlarge nodes ($21.01/hour) — a 52% cost reduction.&lt;/p&gt;

&lt;p&gt;The trade-off: SSD-resident data has slightly higher latency on first access. If your workload regularly accesses less than 20% of the dataset, data tiering is worth evaluating.&lt;/p&gt;

&lt;p&gt;Data tiering is not available with ElastiCache Serverless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserved Nodes: Up to 55% Off
&lt;/h2&gt;

&lt;p&gt;If your ElastiCache usage is stable, reserved nodes offer steep discounts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Commitment&lt;/th&gt;
&lt;th&gt;Discount vs. On-Demand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1-year, No Upfront&lt;/td&gt;
&lt;td&gt;Up to 48.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-year, Partial Upfront&lt;/td&gt;
&lt;td&gt;Up to 52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-year, All Upfront&lt;/td&gt;
&lt;td&gt;Up to 55%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reserved nodes are size-flexible — you can apply the discount across different node sizes within the same family. If you buy a reservation for cache.r7g.xlarge, it can cover cache.r7g.large nodes proportionally.&lt;/p&gt;

&lt;p&gt;One useful detail: Redis OSS reservations automatically apply to Valkey nodes in the same family and region. Since Valkey is 20% cheaper, you get 20% more value from existing reservations after migrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem: Idle Caches
&lt;/h2&gt;

&lt;p&gt;Here's what actually burns money: caches nobody is using.&lt;/p&gt;

&lt;p&gt;ElastiCache has no scale-to-zero for node-based clusters. A cache with zero hits costs exactly the same as one handling millions of requests. This is the pattern we see most often:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A team provisions a cache for a microservice, then the service is deprecated&lt;/li&gt;
&lt;li&gt;Dev/staging caches left running after the project ends&lt;/li&gt;
&lt;li&gt;A "temporary" cache for a migration that became permanent infrastructure&lt;/li&gt;
&lt;li&gt;A replicated cluster in non-production where a single node would suffice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A three-node cache.r7g.xlarge cluster running idle for a year at Valkey on-demand rates: &lt;strong&gt;$9,186 wasted&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Over-Provisioned Caches Are Nearly as Bad
&lt;/h2&gt;

&lt;p&gt;Beyond idle caches, oversized nodes are the second biggest source of waste. Teams pick a large node type during initial setup, the workload stabilizes at a fraction of capacity, and nobody revisits the sizing.&lt;/p&gt;

&lt;p&gt;A cache.r6g.xlarge running at 6% CPU with active connections is doing real work — but it's doing it on a node that's 3–4× larger than needed. Downsizing from cache.r6g.xlarge to cache.r6g.large can cut costs by 40–50% with no performance impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Spot the Waste
&lt;/h2&gt;

&lt;p&gt;Check these CloudWatch metrics for each cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CacheHits:&lt;/strong&gt; Zero for 14+ days means nothing is reading from this cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CurrConnections:&lt;/strong&gt; Zero means nothing is even connecting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EngineCPUUtilization:&lt;/strong&gt; Consistently under 10% with active connections means the node is oversized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quick CLI inventory of all your ElastiCache clusters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws elasticache describe-cache-clusters &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--show-cache-node-info&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'CacheClusters[*].{
    ClusterId:CacheClusterId,
    Engine:Engine,
    EngineVersion:EngineVersion,
    NodeType:CacheNodeType,
    NumNodes:NumCacheNodes,
    Status:CacheClusterStatus
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of those clusters show an engine version approaching EOL, you're on the clock for an Extended Support surcharge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloudcostwise.io/" rel="noopener noreferrer"&gt;CloudWise&lt;/a&gt; detects idle ElastiCache clusters by analyzing CloudWatch cache hit metrics over 14 days, flags oversized nodes running under 10% CPU, and alerts you when clusters are approaching or already incurring Extended Support surcharges. Three detectors, one scan.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloudcostwise.io/" rel="noopener noreferrer"&gt;CloudWise&lt;/a&gt; automates AWS cost analysis across 180+ waste detectors. Try it at &lt;a href="https://cloudcostwise.io/" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>elasticache</category>
      <category>redis</category>
      <category>valkey</category>
    </item>
    <item>
      <title>How Timestream Actually Bills: A Breakdown for Engineers</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Thu, 09 Apr 2026 13:56:59 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/how-timestream-actually-bills-a-breakdown-for-engineers-56b3</link>
      <guid>https://dev.to/cloudwiseteam/how-timestream-actually-bills-a-breakdown-for-engineers-56b3</guid>
      <description>&lt;p&gt;Timestream can look simple on the bill until you break down the line items. Most teams think in terms of "stored data," but Amazon Timestream for LiveAnalytics is billed across multiple meters that move independently.&lt;/p&gt;

&lt;p&gt;If you only watch one number, you can miss where most of the spend actually comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: Know Which Timestream Product You Are Using
&lt;/h2&gt;

&lt;p&gt;AWS now has two Timestream offerings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Timestream for LiveAnalytics&lt;/strong&gt; (serverless, billed by writes, query compute, memory store, magnetic store)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Timestream for InfluxDB&lt;/strong&gt; (managed InfluxDB, billed by DB instance-hours and storage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post focuses on &lt;strong&gt;Timestream for LiveAnalytics&lt;/strong&gt;, where most billing misunderstandings happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LiveAnalytics Billing Model (What Actually Ticks)
&lt;/h2&gt;

&lt;p&gt;For Timestream for LiveAnalytics, AWS charges separately for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Writes&lt;/strong&gt;: billed by amount of data written (rounded to nearest KiB), often shown in pricing examples as a per-million 1 KiB write unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries&lt;/strong&gt;: billed by &lt;strong&gt;Timestream Compute Units (TCUs)&lt;/strong&gt; consumed over time (TCU-hours), not by a flat per-query fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory store&lt;/strong&gt;: billed by &lt;strong&gt;GB-hour&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magnetic store&lt;/strong&gt;: billed by &lt;strong&gt;GB-month&lt;/strong&gt; (with account/region minimums for magnetic storage usage).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the statement "Timestream is $0.10/GB-month" is not accurate for LiveAnalytics. That kind of single storage rate framing is incomplete and often wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Teams Get Surprised
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Query charges are compute-time based, not per query count
&lt;/h3&gt;

&lt;p&gt;A dashboard running one heavy query every few seconds can cost more than many lightweight queries. Query cost follows TCU consumption and duration.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Memory retention is expensive relative to magnetic retention
&lt;/h3&gt;

&lt;p&gt;Keeping a long retention period in memory store drives GB-hour charges. Moving older data to magnetic store usually lowers storage cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) High write frequency amplifies ingestion cost fast
&lt;/h3&gt;

&lt;p&gt;Small records at high frequency still add up, especially without batching and schema optimization.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) "Idle table" thinking can be misleading
&lt;/h3&gt;

&lt;p&gt;In LiveAnalytics, empty or unused tables are not the main cost driver. Spend usually comes from write volume, query compute, and retained data in memory/magnetic tiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Mental Model for Retention
&lt;/h2&gt;

&lt;p&gt;Retention decisions directly shape spend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short memory retention&lt;/strong&gt; for hot, low-latency workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longer magnetic retention&lt;/strong&gt; for historical analysis&lt;/li&gt;
&lt;li&gt;Keep only what is needed in memory store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your query patterns are mostly historical and not sub-second operational reads, memory retention is often set too high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Cost Review Checklist
&lt;/h2&gt;

&lt;p&gt;Run this monthly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Writes&lt;/strong&gt;: are records batched efficiently? Are you writing unnecessary dimensions/measures?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries&lt;/strong&gt;: which workloads consume most TCU time? Any dashboards refreshing too frequently?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory store&lt;/strong&gt;: is hot retention longer than real operational need?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magnetic store&lt;/strong&gt;: is long-term retention aligned with compliance and analytics requirements?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table lifecycle&lt;/strong&gt;: are stale datasets still retained without business need?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  CLI: Inventory Retention Settings Across Tables
&lt;/h2&gt;

&lt;p&gt;Use this to review memory/magnetic retention quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws timestream-write list-databases &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Databases[].DatabaseName'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\t'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;db&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;aws timestream-write list-tables &lt;span class="nt"&gt;--database-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Tables[].TableName'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\t'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;tbl&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tbl&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
    &lt;/span&gt;aws timestream-write describe-table &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--database-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--table-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tbl&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'{Database:Table.DatabaseName,Table:Table.TableName,MemoryHours:Table.RetentionProperties.MemoryStoreRetentionPeriodInHours,MagneticDays:Table.RetentionProperties.MagneticStoreRetentionPeriodInDays}'&lt;/span&gt;
  &lt;span class="k"&gt;done
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not prove usage by itself, but it gives you the retention map you need before optimizing query and write behavior.&lt;/p&gt;

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

&lt;p&gt;Timestream for LiveAnalytics billing is multi-dimensional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writes&lt;/li&gt;
&lt;li&gt;query compute (TCUs)&lt;/li&gt;
&lt;li&gt;memory store&lt;/li&gt;
&lt;li&gt;magnetic store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you treat it as a single storage bill, you will miss the biggest optimization levers.&lt;/p&gt;

&lt;p&gt;CloudWise helps teams surface these hidden cost patterns and prioritize the fastest savings opportunities across AWS data services.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloudcostwise.io/" rel="noopener noreferrer"&gt;CloudWise&lt;/a&gt; automates AWS cost analysis and waste detection. Try it at &lt;a href="https://cloudcostwise.io/" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>cloudcostoptimization</category>
      <category>timestream</category>
    </item>
    <item>
      <title>The Hidden Costs of Idle EMR Clusters (And How to Stop the Bleed)</title>
      <dc:creator>Rick Wise</dc:creator>
      <pubDate>Thu, 02 Apr 2026 14:36:43 +0000</pubDate>
      <link>https://dev.to/cloudwiseteam/the-hidden-costs-of-idle-emr-clusters-and-how-to-stop-the-bleed-5g2g</link>
      <guid>https://dev.to/cloudwiseteam/the-hidden-costs-of-idle-emr-clusters-and-how-to-stop-the-bleed-5g2g</guid>
      <description>&lt;p&gt;EMR looks simple on the bill. You spin up a cluster, run your Spark jobs, and shut it down. But most teams don't shut it down — and that's where the money disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  EMR Has Two Price Tags
&lt;/h2&gt;

&lt;p&gt;Every EMR instance carries &lt;strong&gt;two charges&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;EC2 instance cost&lt;/strong&gt; — the standard on-demand rate for the instance type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EMR surcharge&lt;/strong&gt; — an additional per-instance-hour fee, typically 20–25% of the EC2 price&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a common analytics instance like &lt;code&gt;m5.xlarge&lt;/code&gt; (4 vCPUs, 16 GB RAM) in us-east-1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Hourly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;td&gt;$0.192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMR surcharge&lt;/td&gt;
&lt;td&gt;$0.048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.240/hr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 5-node cluster of &lt;code&gt;m5.xlarge&lt;/code&gt; instances costs &lt;strong&gt;$1.20/hr&lt;/strong&gt; — roughly &lt;strong&gt;$876/month&lt;/strong&gt; if left running. That's just compute. Storage is extra.&lt;/p&gt;

&lt;p&gt;Most teams focus on the EC2 line item and completely miss the EMR surcharge. It doesn't show up as a separate line — it's bundled into the "Amazon Elastic MapReduce" charge on your bill, and it adds up fast across multiple clusters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The EBS Trap
&lt;/h2&gt;

&lt;p&gt;Every EMR node gets EBS volumes attached. The default root volume is typically 10–15 GB, but core and task nodes often get larger volumes for HDFS or local shuffle storage.&lt;/p&gt;

&lt;p&gt;Current EBS pricing in us-east-1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Volume Type&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gp3 (default for new clusters)&lt;/td&gt;
&lt;td&gt;$0.08/GB-month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gp2 (legacy default)&lt;/td&gt;
&lt;td&gt;$0.10/GB-month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;io1 (provisioned IOPS)&lt;/td&gt;
&lt;td&gt;$0.125/GB-month + $0.065/IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 5-node cluster with 100 GB gp3 per node adds &lt;strong&gt;$40/month&lt;/strong&gt; in storage alone. Not huge — but it never stops charging, even when the cluster is idle.&lt;/p&gt;

&lt;p&gt;The real problem isn't the per-GB rate. It's that &lt;strong&gt;EBS charges continue as long as the cluster exists&lt;/strong&gt;, regardless of whether any jobs are running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idle Cluster Problem
&lt;/h2&gt;

&lt;p&gt;Here's the scenario that burns money: a cluster in &lt;code&gt;WAITING&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;EMR clusters have three relevant states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RUNNING&lt;/strong&gt; — actively executing steps (Spark, Hive, Presto jobs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WAITING&lt;/strong&gt; — cluster is up, all steps are complete, waiting for new work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TERMINATED&lt;/strong&gt; — shut down, no charges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;WAITING&lt;/code&gt; state is the silent budget killer. The cluster is fully provisioned — all EC2 instances running, all EBS volumes attached, EMR surcharge ticking — but doing zero work. It's an idle engine burning fuel in a parked car.&lt;/p&gt;

&lt;p&gt;This happens more often than you'd think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dev/test clusters&lt;/strong&gt; spun up for debugging, never terminated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled pipelines&lt;/strong&gt; where the cluster outlives the job by hours or days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Keep-alive" clusters&lt;/strong&gt; left running for ad-hoc queries that happen once a week&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed termination&lt;/strong&gt; where auto-termination was configured but a step error left the cluster hanging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 10-node &lt;code&gt;r5.2xlarge&lt;/code&gt; cluster in WAITING state costs roughly &lt;strong&gt;$5,256/month&lt;/strong&gt; — EC2 ($0.504/hr × 10 × 730) plus EMR surcharge ($0.126/hr × 10 × 730) plus EBS. For processing zero bytes of data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Actually Check
&lt;/h2&gt;

&lt;p&gt;If you want to audit your EMR spend, focus on three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Clusters in WAITING state for more than 24 hours&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;aws emr list-clusters &lt;span class="nt"&gt;--active&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Clusters[?Status.State==`WAITING`]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any cluster that's been waiting more than a day is almost certainly forgotten. Check the &lt;code&gt;ReadyDateTime&lt;/code&gt; in the timeline to see how long it's been idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Long-running clusters with no recent steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some teams run "persistent" EMR clusters for interactive workloads (Jupyter, Presto). These are valid — but they should be right-sized. Check &lt;code&gt;list-steps&lt;/code&gt; to see when the last step actually ran.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws emr list-steps &lt;span class="nt"&gt;--cluster-id&lt;/span&gt; j-XXXXX &lt;span class="nt"&gt;--step-states&lt;/span&gt; COMPLETED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Steps[0].Status.Timeline.EndDateTime'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the last step completed weeks ago, the cluster is waste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Auto-termination configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EMR supports auto-termination after idle timeout. If your clusters don't have this enabled, you're one forgotten SSH session away from a surprise bill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws emr describe-cluster &lt;span class="nt"&gt;--cluster-id&lt;/span&gt; j-XXXXX &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Cluster.AutoTerminationPolicy'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;For batch workloads, the answer is straightforward: use &lt;strong&gt;transient clusters&lt;/strong&gt;. Spin up, process, terminate. EMR's step execution mode does this automatically — the cluster terminates after the last step completes.&lt;/p&gt;

&lt;p&gt;For interactive workloads, set aggressive auto-termination policies (1–2 hours of idle time) and right-size instance types based on actual utilization, not peak estimates from six months ago.&lt;/p&gt;

&lt;p&gt;And tag everything. You can't optimize what you can't attribute. Use &lt;code&gt;aws:elasticmapreduce:editor-id&lt;/code&gt; and custom cost allocation tags to tie clusters back to teams and projects.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CloudWise detects idle and long-running EMR clusters automatically, flags the monthly waste, and generates remediation plans. Try it at &lt;a href="https://cloudcostwise.io" rel="noopener noreferrer"&gt;cloudcostwise.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>costoptimization</category>
      <category>devops</category>
      <category>bigdata</category>
    </item>
  </channel>
</rss>
