DEV Community

Cover image for 7 Days, $0.02 in AWS Costs, and 3 Lessons I Didn't Expect — A Post-Launch Retrospective

7 Days, $0.02 in AWS Costs, and 3 Lessons I Didn't Expect — A Post-Launch Retrospective

A week ago I launched dontdothat.click — a privacy-first AI habit tracker that runs entirely in the browser. I wrote about the build process in my last post. The stack is dead simple: S3 + CloudFront + Route 53. Total infrastructure budget: $3.

Now I have a week of real data. Some of it confirmed what I expected. Some of it didn't.

The Numbers

Let's start with what everyone wants to know:

Route 53 hosted zone:    $0.50  (flat monthly)
S3 storage:              $0.00  (35KB of files, lol)
S3 requests:             $0.01
CloudFront transfer:     $0.01
CloudFront requests:     $0.00
ACM certificate:         $0.00
────────────────────────────────
Total week 1:            $0.52
Annualized:              ~$27/year (mostly the hosted zone)
Enter fullscreen mode Exit fullscreen mode

The hosted zone ($0.50/month = $6/year) costs more than the domain itself ($3/year). That's... not something the tutorials mention. If I were doing this again with an even tighter budget, I'd consider whether I really need Route 53 for DNS or if the registrar's free DNS would work. Spoiler: for a static site, it absolutely would. But CloudFront + Route 53 alias records are so seamless that the $6/year is worth it.

Lesson 1: CloudFront Cache Invalidation Is Your Deploy Pipeline

I didn't set up CI/CD. It's three static files — I don't need a pipeline. What I do need is to remember to invalidate the CloudFront cache after every S3 upload.

The first time I pushed a bug fix, I spent 10 minutes wondering why the site wasn't updating. Then I remembered:

# Upload
aws s3 sync . s3://dontdothat.click/ \
    --exclude ".git/*" --exclude "*.md"

# THIS IS THE STEP YOU'LL FORGET
aws cloudfront create-invalidation \
    --distribution-id E1N9Y6SV8RFQ9Q \
    --paths "/*"
Enter fullscreen mode Exit fullscreen mode

After the second time I forgot, I wrote a one-liner:

# deploy.sh — the world's simplest CI/CD
aws s3 sync . s3://dontdothat.click/ --exclude ".git/*" --exclude "*.md" \
  && aws cloudfront create-invalidation --distribution-id E1N9Y6SV8RFQ9Q --paths "/*"
Enter fullscreen mode Exit fullscreen mode

Two things I learned about invalidation:

  • The first 1,000 invalidation paths per month are free. Using /* counts as one path. So even if you deploy 50 times a day, it costs nothing.
  • Invalidation takes 1-3 minutes. This means there's a window where some users see the old version and others see the new one. For a side project this doesn't matter, but it's good to know.

If this grows, I'd switch to versioned filenames (app.v2.js) instead of invalidation. But for now, the one-liner is perfect.

Lesson 2: The ACM Certificate Dance

This one caught me off guard. When I launched, I requested an ACM certificate with:

aws acm request-certificate \
    --domain-name dontdothat.click \
    --validation-method DNS
Enter fullscreen mode Exit fullscreen mode

DNS validation was straightforward — add a CNAME, wait. But "wait" turned out to be 20+ minutes for a brand-new domain. The docs say "up to 30 minutes" but most blog posts describe it as "a few minutes." For an established domain, sure. For a domain registered 10 minutes ago? Expect the full 30.

Also: a wildcard certificate (*.example.com) does NOT cover the bare domain (example.com). This feels obvious in hindsight, but I hit the CloudFront InvalidViewerCertificate err
or before I realized my cert only covered the wildcard. The fix:

aws acm request-certificate \
    --domain-name dontdothat.click \
    --subject-alternative-names "dontdothat.click" "*.dontdothat.click" \
    --validation-method DNS
Enter fullscreen mode Exit fullscreen mode

Note: --subject-alternative-names, not a second --domain-name flag. The CLI silently overwrites if you pass --domain-name twice. No error, just a cert that doesn't cover what you think it covers.

Lesson 3: S3 Website Hosting Has a Public Access Trap

Creating the bucket is one command. Making it actually serve public web content is three:

# Step 1: Create bucket
aws s3api create-bucket --bucket dontdothat.click

# Step 2: Disable the Block Public Access defaults
# (THIS MUST COME BEFORE the bucket policy)
aws s3api put-public-access-block --bucket dontdothat.click \
    --public-access-block-configuration \
    "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"

# Step 3: NOW you can set the public read policy
aws s3api put-bucket-policy --bucket dontdothat.click \
    --policy '{ ... "Action": "s3:GetObject" ... }'
Enter fullscreen mode Exit fullscreen mode

If you do step 3 before step 2, you get AccessDenied — not because your IAM permissions are wrong, but because S3's Block Public Access feature (enabled by default since 2023) rejects p
ublic policies at the bucket level. The error message mentions "BlockPublicPolicy" but doesn't tell you what to do about it.

This is actually a good security default. But it turns every "host a static site on S3" tutorial into a minefield if it was written before 2023.

The Unexpected Thing

The biggest surprise wasn't technical — it was the traffic pattern. The site got a small spike from Reddit on day 1, then dropped to near-zero. Expected. But starting around day 3, I noticed a slow trickle of organic traffic. People were sharing the direct URL in group chats and Discord servers.

dontdothat.click as a domain name turned out to be the best marketing decision I made. People share it because the URL itself is funny. I've seen it dropped in conversations with zero c
ontext — just the bare URL — and people click it because they're curious what "dontdothat.click" could possibly be.

The $3 domain is generating more word-of-mouth than any Reddit post could.

What's Next

Honestly? Nothing. That's the point. It's a static site on S3 with a CDN in front of it. There's nothing to maintain, nothing to patch, no servers to restart at 3am. The AI model is hosted by Hugging Face and cached in users' browsers. The only moving part is AdSense, and Google manages that.

This is what "passive" should actually mean in "passive income." Not "I check on it every day" — but "I genuinely forget it exists for a week and nothing breaks."

The total cost to run this for a year will be about $30. One good day of ad revenue covers that. Everything else is profit — or more accurately, beer money. But it's beer money I did zero
work for after launch night.

And that's the real lesson: the best architecture for a side project is the one you can walk away from.


Previous post: How I Used AI to Build, Name, and Ship a Privacy-First Product in One Night — for $3

The site: dontdothat.click

Top comments (0)